aboutsummaryrefslogtreecommitdiff
path: root/Tests/NorgKitTests/Parsers/TaskScannerTests.swift
blob: 58226fdc8772d4401ae3dee63a06a75176b3438e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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")
  }
}