refactor: organize tests and dry (#49)
* refactor(store): convert singleton to Store.new() factory
Problem: store.lua used module-level _data singleton, making
project-local stores impossible and creating hidden global state.
Solution: introduce Store metatable with all operations as instance
methods. M.new(path) constructs an instance; M.resolve_path()
searches upward for .pending.json and falls back to
config.get().data_path. Singleton module API is removed.
* refactor(diff): accept store instance as parameter
Problem: diff.apply called store singleton methods directly, coupling
it to global state and preventing use with project-local stores.
Solution: change signature to apply(lines, s, hidden_ids?) where s is
a pending.Store instance. All store operations now go through s.
* refactor(buffer): add set_store/store accessors, drop singleton dep
Problem: buffer.lua imported store directly and called singleton
methods, preventing it from working with per-project store instances.
Solution: add module-level _store, M.set_store(s), and M.store()
accessors. open() and render() use _store instead of the singleton.
init.lua will call buffer.set_store(s) before buffer.open().
* refactor(complete,health,sync,plugin): update callers to store instance API
Problem: complete.lua, health.lua, sync/gcal.lua, and plugin/pending.lua
all called singleton store methods directly.
Solution: complete.lua uses buffer.store() for category lookups;
health.lua uses store.new(store.resolve_path()) and reports the
resolved path; gcal.lua calls require('pending').store() for task
access; plugin tab-completion creates ephemeral store instances via
store.new(store.resolve_path()). Add 'init' to the subcommands list.
* feat(init): thread Store instance through init, add :Pending init
Problem: init.lua called singleton store methods throughout, and there
was no way to create a project-local .pending.json file.
Solution: add module-level _store and private get_store() that
lazy-constructs via store.new(store.resolve_path()). Add public
M.store() accessor used by specs and sync backends. M.open() calls
buffer.set_store(get_store()) before buffer.open(). All store
callsites converted to get_store():method(). goto_file() and
add_here() derive the data directory from get_store().path.
Add M.init() which creates .pending.json in cwd and dispatches from
M.command() as ':Pending init'.
* test: update all specs for Store instance API
Problem: every spec used the old singleton API (store.unload(),
store.load(), store.add(), etc.) and diff.apply(lines, hidden).
Solution: lower-level specs (store, diff, views, complete, file) use
s = store.new(path); s:load() directly. Higher-level specs (archive,
edit, filter, status, sync) reset package.loaded['pending'] in
before_each and use pending.store() to access the live instance.
diff.apply calls updated to diff.apply(lines, s, hidden_ids).
* docs(pending): document :Pending init and store resolution
Add *pending-store-resolution* section explaining upward .pending.json
discovery and fallback to the global data_path. Document :Pending init
under COMMANDS. Add a cross-reference from the data_path config field.
* ci: format
* ci: remove unused variable
This commit is contained in:
parent
dbd76d6759
commit
41bda24570
19 changed files with 819 additions and 703 deletions
|
|
@ -1,87 +1,96 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('archive', function()
|
||||
local tmpdir
|
||||
local pending = require('pending')
|
||||
local pending
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
pending.store():load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
|
||||
it('removes done tasks completed more than 30 days ago', function()
|
||||
local t = store.add({ description = 'Old done task' })
|
||||
store.update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Old done task' })
|
||||
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
pending.archive()
|
||||
assert.are.equal(0, #store.active_tasks())
|
||||
assert.are.equal(0, #s:active_tasks())
|
||||
end)
|
||||
|
||||
it('keeps done tasks completed fewer than 30 days ago', function()
|
||||
local s = pending.store()
|
||||
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
||||
local t = store.add({ description = 'Recent done task' })
|
||||
store.update(t.id, { status = 'done', ['end'] = recent_end })
|
||||
local t = s:add({ description = 'Recent done task' })
|
||||
s:update(t.id, { status = 'done', ['end'] = recent_end })
|
||||
pending.archive()
|
||||
local active = store.active_tasks()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
assert.are.equal('Recent done task', active[1].description)
|
||||
end)
|
||||
|
||||
it('respects a custom day count', function()
|
||||
local s = pending.store()
|
||||
local eight_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (8 * 86400))
|
||||
local t = store.add({ description = 'Old for 7 days' })
|
||||
store.update(t.id, { status = 'done', ['end'] = eight_days_ago })
|
||||
local t = s:add({ description = 'Old for 7 days' })
|
||||
s:update(t.id, { status = 'done', ['end'] = eight_days_ago })
|
||||
pending.archive(7)
|
||||
assert.are.equal(0, #store.active_tasks())
|
||||
assert.are.equal(0, #s:active_tasks())
|
||||
end)
|
||||
|
||||
it('keeps tasks within the custom day cutoff', function()
|
||||
local s = pending.store()
|
||||
local five_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
||||
local t = store.add({ description = 'Recent for 7 days' })
|
||||
store.update(t.id, { status = 'done', ['end'] = five_days_ago })
|
||||
local t = s:add({ description = 'Recent for 7 days' })
|
||||
s:update(t.id, { status = 'done', ['end'] = five_days_ago })
|
||||
pending.archive(7)
|
||||
local active = store.active_tasks()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
end)
|
||||
|
||||
it('never archives pending tasks regardless of age', function()
|
||||
store.add({ description = 'Still pending' })
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Still pending' })
|
||||
pending.archive()
|
||||
local active = store.active_tasks()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
assert.are.equal('pending', active[1].status)
|
||||
end)
|
||||
|
||||
it('removes deleted tasks past the cutoff', function()
|
||||
local t = store.add({ description = 'Old deleted task' })
|
||||
store.update(t.id, { status = 'deleted', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Old deleted task' })
|
||||
s:update(t.id, { status = 'deleted', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
pending.archive()
|
||||
local all = store.tasks()
|
||||
local all = s:tasks()
|
||||
assert.are.equal(0, #all)
|
||||
end)
|
||||
|
||||
it('keeps deleted tasks within the cutoff', function()
|
||||
local s = pending.store()
|
||||
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
||||
local t = store.add({ description = 'Recent deleted' })
|
||||
store.update(t.id, { status = 'deleted', ['end'] = recent_end })
|
||||
local t = s:add({ description = 'Recent deleted' })
|
||||
s:update(t.id, { status = 'deleted', ['end'] = recent_end })
|
||||
pending.archive()
|
||||
local all = store.tasks()
|
||||
local all = s:tasks()
|
||||
assert.are.equal(1, #all)
|
||||
end)
|
||||
|
||||
it('reports the correct count in vim.notify', function()
|
||||
local s = pending.store()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, ...)
|
||||
|
|
@ -89,11 +98,11 @@ describe('archive', function()
|
|||
return orig_notify(msg, ...)
|
||||
end
|
||||
|
||||
local t1 = store.add({ description = 'Old 1' })
|
||||
local t2 = store.add({ description = 'Old 2' })
|
||||
store.add({ description = 'Keep' })
|
||||
store.update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
store.update(t2.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
local t1 = s:add({ description = 'Old 1' })
|
||||
local t2 = s:add({ description = 'Old 2' })
|
||||
s:add({ description = 'Keep' })
|
||||
s:update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
s:update(t2.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
|
||||
pending.archive()
|
||||
|
||||
|
|
@ -110,16 +119,17 @@ describe('archive', function()
|
|||
end)
|
||||
|
||||
it('leaves only kept tasks in store.active_tasks after archive', function()
|
||||
local t1 = store.add({ description = 'Old done' })
|
||||
store.add({ description = 'Keep pending' })
|
||||
local s = pending.store()
|
||||
local t1 = s:add({ description = 'Old done' })
|
||||
s:add({ description = 'Keep pending' })
|
||||
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
||||
local t3 = store.add({ description = 'Keep recent done' })
|
||||
store.update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
store.update(t3.id, { status = 'done', ['end'] = recent_end })
|
||||
local t3 = s:add({ description = 'Keep recent done' })
|
||||
s:update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
s:update(t3.id, { status = 'done', ['end'] = recent_end })
|
||||
|
||||
pending.archive()
|
||||
|
||||
local active = store.active_tasks()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(2, #active)
|
||||
local descs = {}
|
||||
for _, task in ipairs(active) do
|
||||
|
|
@ -130,11 +140,11 @@ describe('archive', function()
|
|||
end)
|
||||
|
||||
it('persists archived tasks to disk after unload/reload', function()
|
||||
local t = store.add({ description = 'Archived task' })
|
||||
store.update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Archived task' })
|
||||
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
pending.archive()
|
||||
store.unload()
|
||||
store.load()
|
||||
assert.are.equal(0, #store.active_tasks())
|
||||
s:load()
|
||||
assert.are.equal(0, #s:active_tasks())
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local buffer = require('pending.buffer')
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('complete', function()
|
||||
local tmpdir
|
||||
local s
|
||||
local complete = require('pending.complete')
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
buffer.set_store(s)
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
buffer.set_store(nil)
|
||||
end)
|
||||
|
||||
describe('findstart', function()
|
||||
|
|
@ -66,9 +68,9 @@ describe('complete', function()
|
|||
|
||||
describe('completions', function()
|
||||
it('returns existing categories for cat:', function()
|
||||
store.add({ description = 'A', category = 'Work' })
|
||||
store.add({ description = 'B', category = 'Home' })
|
||||
store.add({ description = 'C', category = 'Work' })
|
||||
s:add({ description = 'A', category = 'Work' })
|
||||
s:add({ description = 'B', category = 'Home' })
|
||||
s:add({ description = 'C', category = 'Work' })
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat: x' })
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
|
|
@ -85,8 +87,8 @@ describe('complete', function()
|
|||
end)
|
||||
|
||||
it('filters categories by base', function()
|
||||
store.add({ description = 'A', category = 'Work' })
|
||||
store.add({ description = 'B', category = 'Home' })
|
||||
s:add({ description = 'A', category = 'Work' })
|
||||
s:add({ description = 'B', category = 'Home' })
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat:W' })
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('diff', function()
|
||||
local tmpdir
|
||||
local s
|
||||
local diff = require('pending.diff')
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
describe('parse_buffer', function()
|
||||
|
|
@ -107,121 +103,112 @@ describe('diff', function()
|
|||
'- [ ] First task',
|
||||
'- [ ] Second task',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal(2, #tasks)
|
||||
assert.are.equal('First task', tasks[1].description)
|
||||
assert.are.equal('Second task', tasks[2].description)
|
||||
end)
|
||||
|
||||
it('deletes tasks removed from buffer', function()
|
||||
store.add({ description = 'Keep me' })
|
||||
store.add({ description = 'Delete me' })
|
||||
store.save()
|
||||
s:add({ description = 'Keep me' })
|
||||
s:add({ description = 'Delete me' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Keep me',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local active = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
assert.are.equal('Keep me', active[1].description)
|
||||
local deleted = store.get(2)
|
||||
local deleted = s:get(2)
|
||||
assert.are.equal('deleted', deleted.status)
|
||||
end)
|
||||
|
||||
it('updates modified tasks', function()
|
||||
store.add({ description = 'Original' })
|
||||
store.save()
|
||||
s:add({ description = 'Original' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Renamed',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('Renamed', task.description)
|
||||
end)
|
||||
|
||||
it('updates modified when description is renamed', function()
|
||||
local t = store.add({ description = 'Original', category = 'Inbox' })
|
||||
local t = s:add({ description = 'Original', category = 'Inbox' })
|
||||
t.modified = '2020-01-01T00:00:00Z'
|
||||
store.save()
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Renamed',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('Renamed', task.description)
|
||||
assert.is_not.equal('2020-01-01T00:00:00Z', task.modified)
|
||||
end)
|
||||
|
||||
it('handles duplicate ids as copies', function()
|
||||
store.add({ description = 'Original' })
|
||||
store.save()
|
||||
s:add({ description = 'Original' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Original',
|
||||
'/1/- [ ] Copy of original',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal(2, #tasks)
|
||||
end)
|
||||
|
||||
it('moves tasks between categories', function()
|
||||
store.add({ description = 'Moving task', category = 'Inbox' })
|
||||
store.save()
|
||||
s:add({ description = 'Moving task', category = 'Inbox' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Work',
|
||||
'/1/- [ ] Moving task',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('Work', task.category)
|
||||
end)
|
||||
|
||||
it('does not update modified when task is unchanged', function()
|
||||
store.add({ description = 'Stable task', category = 'Inbox' })
|
||||
store.save()
|
||||
s:add({ description = 'Stable task', category = 'Inbox' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Stable task',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local modified_after_first = store.get(1).modified
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local modified_after_first = s:get(1).modified
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal(modified_after_first, task.modified)
|
||||
end)
|
||||
|
||||
it('clears due when removed from buffer line', function()
|
||||
store.add({ description = 'Pay bill', due = '2026-03-15' })
|
||||
store.save()
|
||||
s:add({ description = 'Pay bill', due = '2026-03-15' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Pay bill',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.is_nil(task.due)
|
||||
end)
|
||||
|
||||
|
|
@ -230,39 +217,36 @@ describe('diff', function()
|
|||
'## Inbox',
|
||||
'- [ ] Take out trash rec:weekly',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal(1, #tasks)
|
||||
assert.are.equal('weekly', tasks[1].recur)
|
||||
end)
|
||||
|
||||
it('updates recur field when changed inline', function()
|
||||
store.add({ description = 'Task', recur = 'daily' })
|
||||
store.save()
|
||||
s:add({ description = 'Task', recur = 'daily' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Todo',
|
||||
'/1/- [ ] Task rec:weekly',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('weekly', task.recur)
|
||||
end)
|
||||
|
||||
it('clears recur when token removed from line', function()
|
||||
store.add({ description = 'Task', recur = 'daily' })
|
||||
store.save()
|
||||
s:add({ description = 'Task', recur = 'daily' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Todo',
|
||||
'/1/- [ ] Task',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.is_nil(task.recur)
|
||||
end)
|
||||
|
||||
|
|
@ -271,25 +255,23 @@ describe('diff', function()
|
|||
'## Inbox',
|
||||
'- [ ] Water plants rec:!weekly',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal('weekly', tasks[1].recur)
|
||||
assert.are.equal('completion', tasks[1].recur_mode)
|
||||
end)
|
||||
|
||||
it('clears priority when [N] is removed from buffer line', function()
|
||||
store.add({ description = 'Task name', priority = 1 })
|
||||
store.save()
|
||||
s:add({ description = 'Task name', priority = 1 })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Task name',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal(0, task.priority)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,32 +1,34 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('edit', function()
|
||||
local tmpdir
|
||||
local pending = require('pending')
|
||||
local pending
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
pending.store():load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
|
||||
it('sets due date with resolve_date vocabulary', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'due:tomorrow')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
local today = os.date('*t') --[[@as osdate]]
|
||||
local expected =
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 }))
|
||||
|
|
@ -34,111 +36,123 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('sets due date with literal YYYY-MM-DD', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'due:2026-06-15')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('2026-06-15', updated.due)
|
||||
end)
|
||||
|
||||
it('sets category', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('Work', updated.category)
|
||||
end)
|
||||
|
||||
it('adds priority', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '+!')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal(1, updated.priority)
|
||||
end)
|
||||
|
||||
it('removes priority', function()
|
||||
local t = store.add({ description = 'Task one', priority = 1 })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', priority = 1 })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-!')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal(0, updated.priority)
|
||||
end)
|
||||
|
||||
it('removes due date', function()
|
||||
local t = store.add({ description = 'Task one', due = '2026-06-15' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', due = '2026-06-15' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-due')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated.due)
|
||||
end)
|
||||
|
||||
it('removes category', function()
|
||||
local t = store.add({ description = 'Task one', category = 'Work' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', category = 'Work' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-cat')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated.category)
|
||||
end)
|
||||
|
||||
it('sets recurrence', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'rec:weekly')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('weekly', updated.recur)
|
||||
assert.is_nil(updated.recur_mode)
|
||||
end)
|
||||
|
||||
it('sets completion-based recurrence', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'rec:!daily')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('daily', updated.recur)
|
||||
assert.are.equal('completion', updated.recur_mode)
|
||||
end)
|
||||
|
||||
it('removes recurrence', function()
|
||||
local t = store.add({ description = 'Task one', recur = 'weekly', recur_mode = 'scheduled' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', recur = 'weekly', recur_mode = 'scheduled' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-rec')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated.recur)
|
||||
assert.is_nil(updated.recur_mode)
|
||||
end)
|
||||
|
||||
it('applies multiple operations at once', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'due:today cat:Errands +!')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal(os.date('%Y-%m-%d'), updated.due)
|
||||
assert.are.equal('Errands', updated.category)
|
||||
assert.are.equal(1, updated.priority)
|
||||
end)
|
||||
|
||||
it('pushes to undo stack', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local stack_before = #store.undo_stack()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local stack_before = #s:undo_stack()
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
assert.are.equal(stack_before + 1, #store.undo_stack())
|
||||
assert.are.equal(stack_before + 1, #s:undo_stack())
|
||||
end)
|
||||
|
||||
it('persists changes to disk', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
store.unload()
|
||||
store.load()
|
||||
local updated = store.get(t.id)
|
||||
s:load()
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('Work', updated.category)
|
||||
end)
|
||||
|
||||
it('errors on unknown task ID', function()
|
||||
store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -152,8 +166,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('errors on invalid date', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -167,8 +182,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('errors on unknown operation token', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -182,8 +198,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('errors on invalid recurrence pattern', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -197,8 +214,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('errors when no operations given', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -238,8 +256,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('shows feedback message on success', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -255,12 +274,14 @@ describe('edit', function()
|
|||
it('respects custom date_syntax', function()
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json', date_syntax = 'by' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
local s = pending.store()
|
||||
s:load()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'by:tomorrow')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
local today = os.date('*t') --[[@as osdate]]
|
||||
local expected =
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 }))
|
||||
|
|
@ -270,32 +291,36 @@ describe('edit', function()
|
|||
it('respects custom recur_syntax', function()
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json', recur_syntax = 'repeat' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
local s = pending.store()
|
||||
s:load()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'repeat:weekly')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('weekly', updated.recur)
|
||||
end)
|
||||
|
||||
it('does not modify store on error', function()
|
||||
local t = store.add({ description = 'Task one', category = 'Original' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', category = 'Original' })
|
||||
s:save()
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function() end
|
||||
pending.edit(tostring(t.id), 'due:notadate')
|
||||
vim.notify = orig_notify
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('Original', updated.category)
|
||||
assert.is_nil(updated.due)
|
||||
end)
|
||||
|
||||
it('sets due date with datetime format', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'due:tomorrow@14:00')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
local today = os.date('*t') --[[@as osdate]]
|
||||
local expected =
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 }))
|
||||
|
|
|
|||
|
|
@ -8,21 +8,25 @@ local views = require('pending.views')
|
|||
|
||||
describe('file token', function()
|
||||
local tmpdir
|
||||
local s
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
end)
|
||||
|
||||
describe('parse.body', function()
|
||||
|
|
@ -78,89 +82,88 @@ describe('file token', function()
|
|||
|
||||
describe('diff reconciliation', function()
|
||||
it('stores file field in _extra on write', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one file:src/auth.lua:42',
|
||||
}
|
||||
diff.apply(lines)
|
||||
local updated = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_not_nil(updated._extra)
|
||||
assert.are.equal('src/auth.lua:42', updated._extra.file)
|
||||
end)
|
||||
|
||||
it('updates file field when token changes', function()
|
||||
local t = store.add({ description = 'Task one', _extra = { file = 'old.lua:1' } })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one', _extra = { file = 'old.lua:1' } })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one file:new.lua:99',
|
||||
}
|
||||
diff.apply(lines)
|
||||
local updated = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('new.lua:99', updated._extra.file)
|
||||
end)
|
||||
|
||||
it('clears file field when token is removed from line', function()
|
||||
local t = store.add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one',
|
||||
}
|
||||
diff.apply(lines)
|
||||
local updated = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated._extra)
|
||||
end)
|
||||
|
||||
it('preserves other _extra fields when file is cleared', function()
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'src/auth.lua:42', _gcal_event_id = 'abc123' },
|
||||
})
|
||||
store.save()
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one',
|
||||
}
|
||||
diff.apply(lines)
|
||||
local updated = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_not_nil(updated._extra)
|
||||
assert.is_nil(updated._extra.file)
|
||||
assert.are.equal('abc123', updated._extra._gcal_event_id)
|
||||
end)
|
||||
|
||||
it('round-trips file field through JSON', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one file:src/auth.lua:42',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local loaded = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local loaded = s:get(t.id)
|
||||
assert.is_not_nil(loaded._extra)
|
||||
assert.are.equal('src/auth.lua:42', loaded._extra.file)
|
||||
end)
|
||||
|
||||
it('accepts optional hidden_ids parameter without error', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one',
|
||||
}
|
||||
assert.has_no_error(function()
|
||||
diff.apply(lines, {})
|
||||
diff.apply(lines, s, {})
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('LineMeta', function()
|
||||
it('category_view populates file field in LineMeta', function()
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'src/auth.lua:42' },
|
||||
})
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local _, meta = views.category_view(tasks)
|
||||
local task_meta = nil
|
||||
for _, m in ipairs(meta) do
|
||||
|
|
@ -174,12 +177,12 @@ describe('file token', function()
|
|||
end)
|
||||
|
||||
it('priority_view populates file field in LineMeta', function()
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'src/auth.lua:42' },
|
||||
})
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local _, meta = views.priority_view(tasks)
|
||||
local task_meta = nil
|
||||
for _, m in ipairs(meta) do
|
||||
|
|
@ -193,9 +196,9 @@ describe('file token', function()
|
|||
end)
|
||||
|
||||
it('file field is nil in LineMeta when task has no file', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local _, meta = views.category_view(tasks)
|
||||
local task_meta = nil
|
||||
for _, m in ipairs(meta) do
|
||||
|
|
@ -212,17 +215,18 @@ describe('file token', function()
|
|||
describe(':Pending edit -file', function()
|
||||
it('clears file reference from task', function()
|
||||
local pending = require('pending')
|
||||
local t = store.add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-file')
|
||||
local updated = store.get(t.id)
|
||||
s:load()
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated._extra)
|
||||
end)
|
||||
|
||||
it('shows feedback when file reference is removed', function()
|
||||
local pending = require('pending')
|
||||
local t = store.add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -236,8 +240,8 @@ describe('file token', function()
|
|||
|
||||
it('does not error when task has no file', function()
|
||||
local pending = require('pending')
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
assert.has_no_error(function()
|
||||
pending.edit(tostring(t.id), '-file')
|
||||
end)
|
||||
|
|
@ -245,13 +249,14 @@ describe('file token', function()
|
|||
|
||||
it('preserves other _extra fields when -file is used', function()
|
||||
local pending = require('pending')
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'src/auth.lua:42', _gcal_event_id = 'abc' },
|
||||
})
|
||||
store.save()
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-file')
|
||||
local updated = store.get(t.id)
|
||||
s:load()
|
||||
local updated = s:get(t.id)
|
||||
assert.is_not_nil(updated._extra)
|
||||
assert.is_nil(updated._extra.file)
|
||||
assert.are.equal('abc', updated._extra._gcal_event_id)
|
||||
|
|
@ -263,9 +268,10 @@ describe('file token', function()
|
|||
local pending = require('pending')
|
||||
local buffer = require('pending.buffer')
|
||||
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
|
||||
buffer.set_store(s)
|
||||
local bufnr = buffer.open()
|
||||
vim.bo[bufnr].filetype = 'pending'
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
|
|
@ -306,12 +312,13 @@ describe('file token', function()
|
|||
local pending = require('pending')
|
||||
local buffer = require('pending.buffer')
|
||||
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'nonexistent/path.lua:1' },
|
||||
})
|
||||
store.save()
|
||||
s:save()
|
||||
|
||||
buffer.set_store(s)
|
||||
local bufnr = buffer.open()
|
||||
vim.bo[bufnr].filetype = 'pending'
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ require('spec.helpers')
|
|||
|
||||
local config = require('pending.config')
|
||||
local diff = require('pending.diff')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('filter', function()
|
||||
local tmpdir
|
||||
|
|
@ -14,32 +13,31 @@ describe('filter', function()
|
|||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
pending = require('pending')
|
||||
buffer = require('pending.buffer')
|
||||
buffer.set_filter({}, {})
|
||||
pending.store():load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
end)
|
||||
|
||||
describe('filter predicates', function()
|
||||
it('cat: hides tasks with non-matching category', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local work_task = nil
|
||||
local home_task = nil
|
||||
for _, t in ipairs(tasks) do
|
||||
|
|
@ -57,13 +55,13 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('cat: hides tasks with no category (default category)', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Inbox task' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Inbox task' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local inbox_task = nil
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.category ~= 'Work' then
|
||||
|
|
@ -75,14 +73,14 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('overdue hides non-overdue tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old task', due = '2020-01-01' })
|
||||
store.add({ description = 'Future task', due = '2099-01-01' })
|
||||
store.add({ description = 'No due task' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old task', due = '2020-01-01' })
|
||||
s:add({ description = 'Future task', due = '2099-01-01' })
|
||||
s:add({ description = 'No due task' })
|
||||
s:save()
|
||||
pending.filter('overdue')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local overdue_task, future_task, nodue_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.due == '2020-01-01' then
|
||||
|
|
@ -101,15 +99,15 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('today hides non-today tasks', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Today task', due = today })
|
||||
store.add({ description = 'Old task', due = '2020-01-01' })
|
||||
store.add({ description = 'Future task', due = '2099-01-01' })
|
||||
store.save()
|
||||
s:add({ description = 'Today task', due = today })
|
||||
s:add({ description = 'Old task', due = '2020-01-01' })
|
||||
s:add({ description = 'Future task', due = '2099-01-01' })
|
||||
s:save()
|
||||
pending.filter('today')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local today_task, old_task, future_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.due == today then
|
||||
|
|
@ -128,13 +126,13 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('priority hides non-priority tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Important', priority = 1 })
|
||||
store.add({ description = 'Normal' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Important', priority = 1 })
|
||||
s:add({ description = 'Normal' })
|
||||
s:save()
|
||||
pending.filter('priority')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local important_task, normal_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.priority and t.priority > 0 then
|
||||
|
|
@ -149,14 +147,14 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('multi-predicate AND: cat:Work + overdue', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work overdue', category = 'Work', due = '2020-01-01' })
|
||||
store.add({ description = 'Work future', category = 'Work', due = '2099-01-01' })
|
||||
store.add({ description = 'Home overdue', category = 'Home', due = '2020-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work overdue', category = 'Work', due = '2020-01-01' })
|
||||
s:add({ description = 'Work future', category = 'Work', due = '2099-01-01' })
|
||||
s:add({ description = 'Home overdue', category = 'Home', due = '2020-01-01' })
|
||||
s:save()
|
||||
pending.filter('cat:Work overdue')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local work_overdue, work_future, home_overdue
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.description == 'Work overdue' then
|
||||
|
|
@ -175,10 +173,10 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('filter clear removes all predicates and hidden ids', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
assert.are.equal(1, #buffer.filter_predicates())
|
||||
pending.filter('clear')
|
||||
|
|
@ -187,9 +185,9 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('filter empty string clears filter', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
assert.are.equal(1, #buffer.filter_predicates())
|
||||
pending.filter('')
|
||||
|
|
@ -197,16 +195,16 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('filter predicates persist across set_filter calls', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
local preds = buffer.filter_predicates()
|
||||
assert.are.equal(1, #preds)
|
||||
assert.are.equal('cat:Work', preds[1])
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local home_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.category == 'Home' then
|
||||
|
|
@ -219,11 +217,11 @@ describe('filter', function()
|
|||
|
||||
describe('diff.apply with hidden_ids', function()
|
||||
it('does not mark hidden tasks as deleted', function()
|
||||
store.load()
|
||||
store.add({ description = 'Visible task' })
|
||||
store.add({ description = 'Hidden task' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Visible task' })
|
||||
s:add({ description = 'Hidden task' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local hidden_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.description == 'Hidden task' then
|
||||
|
|
@ -234,19 +232,18 @@ describe('filter', function()
|
|||
local lines = {
|
||||
'/1/- [ ] Visible task',
|
||||
}
|
||||
diff.apply(lines, hidden_ids)
|
||||
store.unload()
|
||||
store.load()
|
||||
local hidden = store.get(hidden_task.id)
|
||||
diff.apply(lines, s, hidden_ids)
|
||||
s:load()
|
||||
local hidden = s:get(hidden_task.id)
|
||||
assert.are.equal('pending', hidden.status)
|
||||
end)
|
||||
|
||||
it('marks tasks deleted when not hidden and not in buffer', function()
|
||||
store.load()
|
||||
store.add({ description = 'Keep task' })
|
||||
store.add({ description = 'Delete task' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Keep task' })
|
||||
s:add({ description = 'Delete task' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local keep_task, delete_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.description == 'Keep task' then
|
||||
|
|
@ -259,27 +256,25 @@ describe('filter', function()
|
|||
local lines = {
|
||||
'/' .. keep_task.id .. '/- [ ] Keep task',
|
||||
}
|
||||
diff.apply(lines, {})
|
||||
store.unload()
|
||||
store.load()
|
||||
local deleted = store.get(delete_task.id)
|
||||
diff.apply(lines, s, {})
|
||||
s:load()
|
||||
local deleted = s:get(delete_task.id)
|
||||
assert.are.equal('deleted', deleted.status)
|
||||
end)
|
||||
|
||||
it('strips FILTER: line before parsing', function()
|
||||
store.load()
|
||||
store.add({ description = 'My task' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'My task' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local task = tasks[1]
|
||||
local lines = {
|
||||
'FILTER: cat:Work',
|
||||
'/' .. task.id .. '/- [ ] My task',
|
||||
}
|
||||
diff.apply(lines, {})
|
||||
store.unload()
|
||||
store.load()
|
||||
local t = store.get(task.id)
|
||||
diff.apply(lines, s, {})
|
||||
s:load()
|
||||
local t = s:get(task.id)
|
||||
assert.are.equal('pending', t.status)
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ require('spec.helpers')
|
|||
|
||||
local config = require('pending.config')
|
||||
local parse = require('pending.parse')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('status', function()
|
||||
local tmpdir
|
||||
|
|
@ -13,22 +12,20 @@ describe('status', function()
|
|||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
pending.store():load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
|
||||
describe('counts', function()
|
||||
it('returns zeroes for empty store', function()
|
||||
store.load()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
assert.are.equal(0, c.today)
|
||||
|
|
@ -38,48 +35,48 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('counts pending tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'One' })
|
||||
store.add({ description = 'Two' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'One' })
|
||||
s:add({ description = 'Two' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(2, c.pending)
|
||||
end)
|
||||
|
||||
it('counts priority tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Urgent', priority = 1 })
|
||||
store.add({ description = 'Normal' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Urgent', priority = 1 })
|
||||
s:add({ description = 'Normal' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.priority)
|
||||
end)
|
||||
|
||||
it('counts overdue tasks with date-only', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old task', due = '2020-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old task', due = '2020-01-01' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
end)
|
||||
|
||||
it('counts overdue tasks with datetime', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old task', due = '2020-01-01T08:00' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old task', due = '2020-01-01T08:00' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
end)
|
||||
|
||||
it('counts today tasks', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Today task', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Today task', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.today)
|
||||
|
|
@ -87,11 +84,11 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('counts mixed overdue and today', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Overdue', due = '2020-01-01' })
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Overdue', due = '2020-01-01' })
|
||||
s:add({ description = 'Today', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
|
|
@ -99,10 +96,10 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('excludes done tasks', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Done', due = '2020-01-01' })
|
||||
store.update(t.id, { status = 'done' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Done', due = '2020-01-01' })
|
||||
s:update(t.id, { status = 'done' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
|
|
@ -110,10 +107,10 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('excludes deleted tasks', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Deleted', due = '2020-01-01' })
|
||||
store.delete(t.id)
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Deleted', due = '2020-01-01' })
|
||||
s:delete(t.id)
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
|
|
@ -121,9 +118,9 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('excludes someday sentinel', function()
|
||||
store.load()
|
||||
store.add({ description = 'Someday', due = '9999-12-30' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Someday', due = '9999-12-30' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
|
|
@ -132,12 +129,12 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('picks earliest future date as next_due', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Soon', due = '2099-06-01' })
|
||||
store.add({ description = 'Sooner', due = '2099-03-01' })
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Soon', due = '2099-06-01' })
|
||||
s:add({ description = 'Sooner', due = '2099-03-01' })
|
||||
s:add({ description = 'Today', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(today, c.next_due)
|
||||
|
|
@ -161,7 +158,6 @@ describe('status', function()
|
|||
},
|
||||
}))
|
||||
f:close()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
local c = pending.counts()
|
||||
|
|
@ -171,35 +167,35 @@ describe('status', function()
|
|||
|
||||
describe('statusline', function()
|
||||
it('returns empty string when nothing actionable', function()
|
||||
store.load()
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('', pending.statusline())
|
||||
end)
|
||||
|
||||
it('formats overdue only', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old', due = '2020-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old', due = '2020-01-01' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('1 overdue', pending.statusline())
|
||||
end)
|
||||
|
||||
it('formats today only', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Today', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('1 today', pending.statusline())
|
||||
end)
|
||||
|
||||
it('formats overdue and today', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Old', due = '2020-01-01' })
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Old', due = '2020-01-01' })
|
||||
s:add({ description = 'Today', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('1 overdue, 1 today', pending.statusline())
|
||||
end)
|
||||
|
|
@ -207,26 +203,26 @@ describe('status', function()
|
|||
|
||||
describe('has_due', function()
|
||||
it('returns false when nothing due', function()
|
||||
store.load()
|
||||
store.add({ description = 'Future', due = '2099-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Future', due = '2099-01-01' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.is_false(pending.has_due())
|
||||
end)
|
||||
|
||||
it('returns true when overdue', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old', due = '2020-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old', due = '2020-01-01' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.is_true(pending.has_due())
|
||||
end)
|
||||
|
||||
it('returns true when today', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Now', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Now', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.is_true(pending.has_due())
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -5,31 +5,30 @@ local store = require('pending.store')
|
|||
|
||||
describe('store', function()
|
||||
local tmpdir
|
||||
local s
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
describe('load', function()
|
||||
it('returns empty data when no file exists', function()
|
||||
local data = store.load()
|
||||
local data = s:load()
|
||||
assert.are.equal(1, data.version)
|
||||
assert.are.equal(1, data.next_id)
|
||||
assert.are.same({}, data.tasks)
|
||||
end)
|
||||
|
||||
it('loads existing data', function()
|
||||
local path = config.get().data_path
|
||||
local path = tmpdir .. '/tasks.json'
|
||||
local f = io.open(path, 'w')
|
||||
f:write(vim.json.encode({
|
||||
version = 1,
|
||||
|
|
@ -52,7 +51,7 @@ describe('store', function()
|
|||
},
|
||||
}))
|
||||
f:close()
|
||||
local data = store.load()
|
||||
local data = s:load()
|
||||
assert.are.equal(3, data.next_id)
|
||||
assert.are.equal(2, #data.tasks)
|
||||
assert.are.equal('Pending one', data.tasks[1].description)
|
||||
|
|
@ -60,7 +59,7 @@ describe('store', function()
|
|||
end)
|
||||
|
||||
it('preserves unknown fields', function()
|
||||
local path = config.get().data_path
|
||||
local path = tmpdir .. '/tasks.json'
|
||||
local f = io.open(path, 'w')
|
||||
f:write(vim.json.encode({
|
||||
version = 1,
|
||||
|
|
@ -77,8 +76,8 @@ describe('store', function()
|
|||
},
|
||||
}))
|
||||
f:close()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.is_not_nil(task._extra)
|
||||
assert.are.equal('hello', task._extra.custom_field)
|
||||
end)
|
||||
|
|
@ -86,9 +85,8 @@ describe('store', function()
|
|||
|
||||
describe('add', function()
|
||||
it('creates a task with incremented id', function()
|
||||
store.load()
|
||||
local t1 = store.add({ description = 'First' })
|
||||
local t2 = store.add({ description = 'Second' })
|
||||
local t1 = s:add({ description = 'First' })
|
||||
local t2 = s:add({ description = 'Second' })
|
||||
assert.are.equal(1, t1.id)
|
||||
assert.are.equal(2, t2.id)
|
||||
assert.are.equal('pending', t1.status)
|
||||
|
|
@ -96,60 +94,54 @@ describe('store', function()
|
|||
end)
|
||||
|
||||
it('uses provided category', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Test', category = 'Work' })
|
||||
local t = s:add({ description = 'Test', category = 'Work' })
|
||||
assert.are.equal('Work', t.category)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('update', function()
|
||||
it('updates fields and sets modified', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Original' })
|
||||
local t = s:add({ description = 'Original' })
|
||||
t.modified = '2025-01-01T00:00:00Z'
|
||||
store.update(t.id, { description = 'Updated' })
|
||||
local updated = store.get(t.id)
|
||||
s:update(t.id, { description = 'Updated' })
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('Updated', updated.description)
|
||||
assert.is_not.equal('2025-01-01T00:00:00Z', updated.modified)
|
||||
end)
|
||||
|
||||
it('sets end timestamp on completion', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Test' })
|
||||
local t = s:add({ description = 'Test' })
|
||||
assert.is_nil(t['end'])
|
||||
store.update(t.id, { status = 'done' })
|
||||
local updated = store.get(t.id)
|
||||
s:update(t.id, { status = 'done' })
|
||||
local updated = s:get(t.id)
|
||||
assert.is_not_nil(updated['end'])
|
||||
end)
|
||||
|
||||
it('does not overwrite id or entry', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Immutable fields' })
|
||||
local t = s:add({ description = 'Immutable fields' })
|
||||
local original_id = t.id
|
||||
local original_entry = t.entry
|
||||
store.update(t.id, { id = 999, entry = 'x' })
|
||||
local updated = store.get(original_id)
|
||||
s:update(t.id, { id = 999, entry = 'x' })
|
||||
local updated = s:get(original_id)
|
||||
assert.are.equal(original_id, updated.id)
|
||||
assert.are.equal(original_entry, updated.entry)
|
||||
end)
|
||||
|
||||
it('does not overwrite end on second completion', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Complete twice' })
|
||||
store.update(t.id, { status = 'done', ['end'] = '2026-01-15T10:00:00Z' })
|
||||
local first_end = store.get(t.id)['end']
|
||||
store.update(t.id, { status = 'done' })
|
||||
local task = store.get(t.id)
|
||||
local t = s:add({ description = 'Complete twice' })
|
||||
s:update(t.id, { status = 'done', ['end'] = '2026-01-15T10:00:00Z' })
|
||||
local first_end = s:get(t.id)['end']
|
||||
s:update(t.id, { status = 'done' })
|
||||
local task = s:get(t.id)
|
||||
assert.are.equal(first_end, task['end'])
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('delete', function()
|
||||
it('marks task as deleted', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'To delete' })
|
||||
store.delete(t.id)
|
||||
local deleted = store.get(t.id)
|
||||
local t = s:add({ description = 'To delete' })
|
||||
s:delete(t.id)
|
||||
local deleted = s:get(t.id)
|
||||
assert.are.equal('deleted', deleted.status)
|
||||
assert.is_not_nil(deleted['end'])
|
||||
end)
|
||||
|
|
@ -157,12 +149,10 @@ describe('store', function()
|
|||
|
||||
describe('save and round-trip', function()
|
||||
it('persists and reloads correctly', function()
|
||||
store.load()
|
||||
store.add({ description = 'Persisted', category = 'Work', priority = 1 })
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
s:add({ description = 'Persisted', category = 'Work', priority = 1 })
|
||||
s:save()
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal(1, #tasks)
|
||||
assert.are.equal('Persisted', tasks[1].description)
|
||||
assert.are.equal('Work', tasks[1].category)
|
||||
|
|
@ -170,7 +160,7 @@ describe('store', function()
|
|||
end)
|
||||
|
||||
it('round-trips unknown fields', function()
|
||||
local path = config.get().data_path
|
||||
local path = tmpdir .. '/tasks.json'
|
||||
local f = io.open(path, 'w')
|
||||
f:write(vim.json.encode({
|
||||
version = 1,
|
||||
|
|
@ -187,45 +177,38 @@ describe('store', function()
|
|||
},
|
||||
}))
|
||||
f:close()
|
||||
store.load()
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:load()
|
||||
s:save()
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('abc123', task._extra._gcal_event_id)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('recurrence fields', function()
|
||||
it('persists recur and recur_mode through round-trip', function()
|
||||
store.load()
|
||||
store.add({ description = 'Recurring', recur = 'weekly', recur_mode = 'scheduled' })
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:add({ description = 'Recurring', recur = 'weekly', recur_mode = 'scheduled' })
|
||||
s:save()
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('weekly', task.recur)
|
||||
assert.are.equal('scheduled', task.recur_mode)
|
||||
end)
|
||||
|
||||
it('persists recur without recur_mode', function()
|
||||
store.load()
|
||||
store.add({ description = 'Simple recur', recur = 'daily' })
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:add({ description = 'Simple recur', recur = 'daily' })
|
||||
s:save()
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('daily', task.recur)
|
||||
assert.is_nil(task.recur_mode)
|
||||
end)
|
||||
|
||||
it('omits recur fields when not set', function()
|
||||
store.load()
|
||||
store.add({ description = 'No recur' })
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:add({ description = 'No recur' })
|
||||
s:save()
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.is_nil(task.recur)
|
||||
assert.is_nil(task.recur_mode)
|
||||
end)
|
||||
|
|
@ -233,11 +216,10 @@ describe('store', function()
|
|||
|
||||
describe('active_tasks', function()
|
||||
it('excludes deleted tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Active' })
|
||||
local t2 = store.add({ description = 'To delete' })
|
||||
store.delete(t2.id)
|
||||
local active = store.active_tasks()
|
||||
s:add({ description = 'Active' })
|
||||
local t2 = s:add({ description = 'To delete' })
|
||||
s:delete(t2.id)
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
assert.are.equal('Active', active[1].description)
|
||||
end)
|
||||
|
|
@ -245,27 +227,24 @@ describe('store', function()
|
|||
|
||||
describe('snapshot', function()
|
||||
it('returns a table of tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Snap one' })
|
||||
store.add({ description = 'Snap two' })
|
||||
local snap = store.snapshot()
|
||||
s:add({ description = 'Snap one' })
|
||||
s:add({ description = 'Snap two' })
|
||||
local snap = s:snapshot()
|
||||
assert.are.equal(2, #snap)
|
||||
end)
|
||||
|
||||
it('returns a copy that does not affect the store', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Original' })
|
||||
local snap = store.snapshot()
|
||||
local t = s:add({ description = 'Original' })
|
||||
local snap = s:snapshot()
|
||||
snap[1].description = 'Mutated'
|
||||
local live = store.get(t.id)
|
||||
local live = s:get(t.id)
|
||||
assert.are.equal('Original', live.description)
|
||||
end)
|
||||
|
||||
it('excludes deleted tasks', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Will be deleted' })
|
||||
store.delete(t.id)
|
||||
local snap = store.snapshot()
|
||||
local t = s:add({ description = 'Will be deleted' })
|
||||
s:delete(t.id)
|
||||
local snap = s:snapshot()
|
||||
assert.are.equal(0, #snap)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('sync', function()
|
||||
local tmpdir
|
||||
|
|
@ -12,7 +11,6 @@ describe('sync', function()
|
|||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
end)
|
||||
|
|
@ -21,7 +19,6 @@ describe('sync', function()
|
|||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,28 +5,27 @@ local store = require('pending.store')
|
|||
|
||||
describe('views', function()
|
||||
local tmpdir
|
||||
local s
|
||||
local views = require('pending.views')
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
describe('category_view', function()
|
||||
it('groups tasks under their category header', function()
|
||||
store.add({ description = 'Task A', category = 'Work' })
|
||||
store.add({ description = 'Task B', category = 'Work' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Task A', category = 'Work' })
|
||||
s:add({ description = 'Task B', category = 'Work' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
assert.are.equal('## Work', lines[1])
|
||||
assert.are.equal('header', meta[1].type)
|
||||
assert.is_true(lines[2]:find('Task A') ~= nil)
|
||||
|
|
@ -34,10 +33,10 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('places pending tasks before done tasks within a category', function()
|
||||
local t1 = store.add({ description = 'Done task', category = 'Work' })
|
||||
store.add({ description = 'Pending task', category = 'Work' })
|
||||
store.update(t1.id, { status = 'done' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t1 = s:add({ description = 'Done task', category = 'Work' })
|
||||
s:add({ description = 'Pending task', category = 'Work' })
|
||||
s:update(t1.id, { status = 'done' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local pending_row, done_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.status == 'pending' then
|
||||
|
|
@ -50,9 +49,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts high-priority tasks before normal tasks within pending group', function()
|
||||
store.add({ description = 'Normal', category = 'Work', priority = 0 })
|
||||
store.add({ description = 'High', category = 'Work', priority = 1 })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Normal', category = 'Work', priority = 0 })
|
||||
s:add({ description = 'High', category = 'Work', priority = 1 })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local high_row, normal_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -68,11 +67,11 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts high-priority tasks before normal tasks within done group', function()
|
||||
local t1 = store.add({ description = 'Done Normal', category = 'Work', priority = 0 })
|
||||
local t2 = store.add({ description = 'Done High', category = 'Work', priority = 1 })
|
||||
store.update(t1.id, { status = 'done' })
|
||||
store.update(t2.id, { status = 'done' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
local t1 = s:add({ description = 'Done Normal', category = 'Work', priority = 0 })
|
||||
local t2 = s:add({ description = 'Done High', category = 'Work', priority = 1 })
|
||||
s:update(t1.id, { status = 'done' })
|
||||
s:update(t2.id, { status = 'done' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local high_row, normal_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -88,9 +87,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('gives each category its own header with blank lines between them', function()
|
||||
store.add({ description = 'Task A', category = 'Work' })
|
||||
store.add({ description = 'Task B', category = 'Personal' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Task A', category = 'Work' })
|
||||
s:add({ description = 'Task B', category = 'Personal' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local headers = {}
|
||||
local blank_found = false
|
||||
for i, m in ipairs(meta) do
|
||||
|
|
@ -105,8 +104,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('formats task lines as /ID/ description', function()
|
||||
store.add({ description = 'My task', category = 'Inbox' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'My task', category = 'Inbox' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local task_line
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -117,8 +116,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('formats priority task lines as /ID/- [!] description', function()
|
||||
store.add({ description = 'Important', category = 'Inbox', priority = 1 })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Important', category = 'Inbox', priority = 1 })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local task_line
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -129,15 +128,15 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sets LineMeta type=header for header lines with correct category', function()
|
||||
store.add({ description = 'T', category = 'School' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'T', category = 'School' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
assert.are.equal('header', meta[1].type)
|
||||
assert.are.equal('School', meta[1].category)
|
||||
end)
|
||||
|
||||
it('sets LineMeta type=task with correct id and status', function()
|
||||
local t = store.add({ description = 'Do something', category = 'Inbox' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Do something', category = 'Inbox' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -150,9 +149,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sets LineMeta type=blank for blank separator lines', function()
|
||||
store.add({ description = 'A', category = 'Work' })
|
||||
store.add({ description = 'B', category = 'Home' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'A', category = 'Work' })
|
||||
s:add({ description = 'B', category = 'Home' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local blank_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'blank' then
|
||||
|
|
@ -166,8 +165,8 @@ describe('views', function()
|
|||
|
||||
it('marks overdue pending tasks with meta.overdue=true', function()
|
||||
local yesterday = os.date('%Y-%m-%d', os.time() - 86400)
|
||||
local t = store.add({ description = 'Overdue task', category = 'Inbox', due = yesterday })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Overdue task', category = 'Inbox', due = yesterday })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -179,8 +178,8 @@ describe('views', function()
|
|||
|
||||
it('does not mark future pending tasks as overdue', function()
|
||||
local tomorrow = os.date('%Y-%m-%d', os.time() + 86400)
|
||||
local t = store.add({ description = 'Future task', category = 'Inbox', due = tomorrow })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Future task', category = 'Inbox', due = tomorrow })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -192,9 +191,9 @@ describe('views', function()
|
|||
|
||||
it('does not mark done tasks with overdue due dates as overdue', function()
|
||||
local yesterday = os.date('%Y-%m-%d', os.time() - 86400)
|
||||
local t = store.add({ description = 'Done late', category = 'Inbox', due = yesterday })
|
||||
store.update(t.id, { status = 'done' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Done late', category = 'Inbox', due = yesterday })
|
||||
s:update(t.id, { status = 'done' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -205,8 +204,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('includes recur in LineMeta for recurring tasks', function()
|
||||
store.add({ description = 'Recurring', category = 'Inbox', recur = 'weekly' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Recurring', category = 'Inbox', recur = 'weekly' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -217,8 +216,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('has nil recur in LineMeta for non-recurring tasks', function()
|
||||
store.add({ description = 'Normal', category = 'Inbox' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Normal', category = 'Inbox' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -231,9 +230,9 @@ describe('views', function()
|
|||
it('respects category_order when set', function()
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json', category_order = { 'Work', 'Inbox' } }
|
||||
config.reset()
|
||||
store.add({ description = 'Inbox task', category = 'Inbox' })
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Inbox task', category = 'Inbox' })
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local first_header, second_header
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'header' then
|
||||
|
|
@ -251,9 +250,9 @@ describe('views', function()
|
|||
it('appends categories not in category_order after ordered ones', function()
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json', category_order = { 'Work' } }
|
||||
config.reset()
|
||||
store.add({ description = 'Errand', category = 'Errands' })
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Errand', category = 'Errands' })
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local headers = {}
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'header' then
|
||||
|
|
@ -265,9 +264,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('preserves insertion order when category_order is empty', function()
|
||||
store.add({ description = 'Alpha task', category = 'Alpha' })
|
||||
store.add({ description = 'Beta task', category = 'Beta' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Alpha task', category = 'Alpha' })
|
||||
s:add({ description = 'Beta task', category = 'Beta' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local headers = {}
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'header' then
|
||||
|
|
@ -281,10 +280,10 @@ describe('views', function()
|
|||
|
||||
describe('priority_view', function()
|
||||
it('places all pending tasks before done tasks', function()
|
||||
local t1 = store.add({ description = 'Done A', category = 'Work' })
|
||||
store.add({ description = 'Pending B', category = 'Work' })
|
||||
store.update(t1.id, { status = 'done' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
local t1 = s:add({ description = 'Done A', category = 'Work' })
|
||||
s:add({ description = 'Pending B', category = 'Work' })
|
||||
s:update(t1.id, { status = 'done' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local last_pending_row, first_done_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -299,9 +298,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts pending tasks by priority desc within pending group', function()
|
||||
store.add({ description = 'Low', category = 'Work', priority = 0 })
|
||||
store.add({ description = 'High', category = 'Work', priority = 1 })
|
||||
local lines, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Low', category = 'Work', priority = 0 })
|
||||
s:add({ description = 'High', category = 'Work', priority = 1 })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local high_row, low_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -316,9 +315,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts pending tasks with due dates before those without', function()
|
||||
store.add({ description = 'No due', category = 'Work' })
|
||||
store.add({ description = 'Has due', category = 'Work', due = '2099-12-31' })
|
||||
local lines, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'No due', category = 'Work' })
|
||||
s:add({ description = 'Has due', category = 'Work', due = '2099-12-31' })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local due_row, nodue_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -333,9 +332,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts pending tasks with earlier due dates before later due dates', function()
|
||||
store.add({ description = 'Later', category = 'Work', due = '2099-12-31' })
|
||||
store.add({ description = 'Earlier', category = 'Work', due = '2050-01-01' })
|
||||
local lines, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Later', category = 'Work', due = '2099-12-31' })
|
||||
s:add({ description = 'Earlier', category = 'Work', due = '2050-01-01' })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local earlier_row, later_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -350,15 +349,15 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('formats task lines as /ID/- [ ] description', function()
|
||||
store.add({ description = 'My task', category = 'Inbox' })
|
||||
local lines, _ = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'My task', category = 'Inbox' })
|
||||
local lines, _ = views.priority_view(s:active_tasks())
|
||||
assert.are.equal('/1/- [ ] My task', lines[1])
|
||||
end)
|
||||
|
||||
it('sets show_category=true for all task meta entries', function()
|
||||
store.add({ description = 'T1', category = 'Work' })
|
||||
store.add({ description = 'T2', category = 'Personal' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'T1', category = 'Work' })
|
||||
s:add({ description = 'T2', category = 'Personal' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
assert.is_true(m.show_category == true)
|
||||
|
|
@ -367,9 +366,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sets meta.category correctly for each task', function()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
local lines, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local categories = {}
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -386,8 +385,8 @@ describe('views', function()
|
|||
|
||||
it('marks overdue pending tasks with meta.overdue=true', function()
|
||||
local yesterday = os.date('%Y-%m-%d', os.time() - 86400)
|
||||
local t = store.add({ description = 'Overdue', category = 'Inbox', due = yesterday })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Overdue', category = 'Inbox', due = yesterday })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -399,8 +398,8 @@ describe('views', function()
|
|||
|
||||
it('does not mark future pending tasks as overdue', function()
|
||||
local tomorrow = os.date('%Y-%m-%d', os.time() + 86400)
|
||||
local t = store.add({ description = 'Future', category = 'Inbox', due = tomorrow })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Future', category = 'Inbox', due = tomorrow })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -412,9 +411,9 @@ describe('views', function()
|
|||
|
||||
it('does not mark done tasks with overdue due dates as overdue', function()
|
||||
local yesterday = os.date('%Y-%m-%d', os.time() - 86400)
|
||||
local t = store.add({ description = 'Done late', category = 'Inbox', due = yesterday })
|
||||
store.update(t.id, { status = 'done' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Done late', category = 'Inbox', due = yesterday })
|
||||
s:update(t.id, { status = 'done' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -425,8 +424,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('includes recur in LineMeta for recurring tasks', function()
|
||||
store.add({ description = 'Recurring', category = 'Inbox', recur = 'daily' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Recurring', category = 'Inbox', recur = 'daily' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -437,8 +436,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('has nil recur in LineMeta for non-recurring tasks', function()
|
||||
store.add({ description = 'Normal', category = 'Inbox' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Normal', category = 'Inbox' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue