import Foundation import Testing @testable import NorgKit struct TaskScannerTests { private let file = URL(filePath: "/tmp/notes.norg") @Test func scansTasksWithLineNumbers() { let content = """ * Project - ( ) first task - (x) done task plain text ~ (-) ordered pending """ let tasks = TaskScanner.scan(content: content, fileURL: file) #expect(tasks.count == 3) #expect(tasks[0].line == 1) #expect(tasks[0].status == .undone) #expect(tasks[0].text == "first task") #expect(tasks[1].status == .done) #expect(tasks[2].status == .pending) } @Test func ignoresNonTaskLines() { let tasks = TaskScanner.scan(content: "- just a list item\n* heading", fileURL: file) #expect(tasks.isEmpty) } @Test func parsesSingleLineIntoTask() { let task = TaskScanner.task(in: "- (!) urgent thing", fileURL: file, line: 9) #expect(task?.line == 9) #expect(task?.status == .urgent) #expect(task?.text == "urgent thing") #expect(task?.fileURL == file) } @Test func parsesSingleLineReturnsNilForNonTask() { #expect(TaskScanner.task(in: "- plain item", fileURL: file, line: 0) == nil) } @Test func scansEveryStatusBearingModifier() { // The Norg spec allows any detached modifier to carry a TODO status. We // model the structural, nestable, and range-able modifiers (`%` // attributes are intentionally not treated as tasks). let content = """ * (x) heading task - ( ) list task ~ (-) ordered task > (!) quote task $ (?) definition task ^ (=) footnote task : (+) table cell task """ let tasks = TaskScanner.scan(content: content, fileURL: file) #expect(tasks.count == 7) #expect( tasks.map(\.status) == [ .done, .undone, .pending, .urgent, .needsInput, .onHold, .recurring, ]) #expect(tasks[3].text == "quote task") #expect(tasks[4].text == "definition task") } @Test func stripsInlineMarkupFromTaskText() { let tasks = TaskScanner.scan(content: "- ( ) buy *milk*", fileURL: file) #expect(tasks.first?.text == "buy milk") } @Test func updatesStatusMarker() { let content = "- ( ) toggle me" let updated = TaskScanner.updatedContent(content, line: 0, to: .done) #expect(updated == "- (x) toggle me") } @Test func updatesStatusPreservingIndentation() { let content = "* Heading\n -- (x) nested" let updated = TaskScanner.updatedContent(content, line: 1, to: .cancelled) #expect(updated == "* Heading\n -- (_) nested") } @Test func updateReturnsNilWhenNoMarker() { #expect(TaskScanner.updatedContent("plain line", line: 0, to: .done) == nil) } @Test func updateReturnsNilForOutOfRangeLine() { #expect(TaskScanner.updatedContent("- ( ) a", line: 5, to: .done) == nil) } @Test func updatesStatusOnLaterLineLeavingRestIntact() { let content = "* heading\n> ( ) quote task\n- plain item" let updated = TaskScanner.updatedContent(content, line: 1, to: .urgent) #expect(updated == "* heading\n> (!) quote task\n- plain item") } @Test func scansCrlfContent() { let tasks = TaskScanner.scan(content: "- ( ) first\r\n- (x) second\r\n", fileURL: file) #expect(tasks.count == 2) #expect(tasks[0].text == "first") #expect(tasks[1].status == .done) } @Test func updatePreservesCrlfLineEndings() { let content = "- ( ) a\r\n- ( ) b" let updated = TaskScanner.updatedContent(content, line: 1, to: .done) #expect(updated == "- ( ) a\r\n- (x) b") } }