require('spec.helpers') local config = require('pending.config') local store = require('pending.store') describe('views', function() local tmpdir 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() 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()) assert.are.equal('Work', lines[1]) assert.are.equal('header', meta[1].type) assert.is_true(lines[2]:find('Task A') ~= nil) assert.is_true(lines[3]:find('Task B') ~= nil) 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 pending_row, done_row for i, m in ipairs(meta) do if m.type == 'task' and m.status == 'pending' then pending_row = i elseif m.type == 'task' and m.status == 'done' then done_row = i end end assert.is_true(pending_row < done_row) 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()) local high_row, normal_row for i, m in ipairs(meta) do if m.type == 'task' then local line = lines[i] if line:find('High') then high_row = i elseif line:find('Normal') then normal_row = i end end end assert.is_true(high_row < normal_row) 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 high_row, normal_row for i, m in ipairs(meta) do if m.type == 'task' then local line = lines[i] if line:find('Done High') then high_row = i elseif line:find('Done Normal') then normal_row = i end end end assert.is_true(high_row < normal_row) 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()) local headers = {} local blank_found = false for i, m in ipairs(meta) do if m.type == 'header' then table.insert(headers, lines[i]) elseif m.type == 'blank' then blank_found = true end end assert.are.equal(2, #headers) assert.is_true(blank_found) 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()) local task_line for i, m in ipairs(meta) do if m.type == 'task' then task_line = lines[i] end end assert.are.equal('/1/ My task', task_line) 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()) local task_line for i, m in ipairs(meta) do if m.type == 'task' then task_line = lines[i] end end assert.are.equal('/1/ ! Important', task_line) 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()) 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 task_meta for _, m in ipairs(meta) do if m.type == 'task' then task_meta = m end end assert.are.equal('task', task_meta.type) assert.are.equal(t.id, task_meta.id) assert.are.equal('pending', task_meta.status) 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()) local blank_meta for _, m in ipairs(meta) do if m.type == 'blank' then blank_meta = m break end end assert.is_not_nil(blank_meta) assert.are.equal('blank', blank_meta.type) end) 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 task_meta for _, m in ipairs(meta) do if m.type == 'task' and m.id == t.id then task_meta = m end end assert.is_true(task_meta.overdue == true) end) 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 task_meta for _, m in ipairs(meta) do if m.type == 'task' and m.id == t.id then task_meta = m end end assert.is_falsy(task_meta.overdue) end) 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 task_meta for _, m in ipairs(meta) do if m.type == 'task' and m.id == t.id then task_meta = m end end assert.is_falsy(task_meta.overdue) end) 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()) local first_header, second_header for i, m in ipairs(meta) do if m.type == 'header' then if not first_header then first_header = lines[i] else second_header = lines[i] end end end assert.are.equal('Work', first_header) assert.are.equal('Inbox', second_header) end) 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()) local headers = {} for i, m in ipairs(meta) do if m.type == 'header' then table.insert(headers, lines[i]) end end assert.are.equal('Work', headers[1]) assert.are.equal('Errands', headers[2]) 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()) local headers = {} for i, m in ipairs(meta) do if m.type == 'header' then table.insert(headers, lines[i]) end end assert.are.equal('Alpha', headers[1]) assert.are.equal('Beta', headers[2]) end) end) 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 last_pending_row, first_done_row for i, m in ipairs(meta) do if m.type == 'task' then if m.status == 'pending' then last_pending_row = i elseif m.status == 'done' and not first_done_row then first_done_row = i end end end assert.is_true(last_pending_row < first_done_row) 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()) local high_row, low_row for i, m in ipairs(meta) do if m.type == 'task' then if lines[i]:find('High') then high_row = i elseif lines[i]:find('Low') then low_row = i end end end assert.is_true(high_row < low_row) 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()) local due_row, nodue_row for i, m in ipairs(meta) do if m.type == 'task' then if lines[i]:find('Has due') then due_row = i elseif lines[i]:find('No due') then nodue_row = i end end end assert.is_true(due_row < nodue_row) 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()) local earlier_row, later_row for i, m in ipairs(meta) do if m.type == 'task' then if lines[i]:find('Earlier') then earlier_row = i elseif lines[i]:find('Later') then later_row = i end end end assert.is_true(earlier_row < later_row) end) it('formats task lines as /ID/ description', function() store.add({ description = 'My task', category = 'Inbox' }) local lines, _ = views.priority_view(store.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()) for _, m in ipairs(meta) do if m.type == 'task' then assert.is_true(m.show_category == true) end end 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()) local categories = {} for i, m in ipairs(meta) do if m.type == 'task' then if lines[i]:find('Work task') then categories['Work task'] = m.category elseif lines[i]:find('Home task') then categories['Home task'] = m.category end end end assert.are.equal('Work', categories['Work task']) assert.are.equal('Home', categories['Home task']) end) 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 task_meta for _, m in ipairs(meta) do if m.type == 'task' and m.id == t.id then task_meta = m end end assert.is_true(task_meta.overdue == true) end) 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 task_meta for _, m in ipairs(meta) do if m.type == 'task' and m.id == t.id then task_meta = m end end assert.is_falsy(task_meta.overdue) end) 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 task_meta for _, m in ipairs(meta) do if m.type == 'task' and m.id == t.id then task_meta = m end end assert.is_falsy(task_meta.overdue) end) end) end)