aboutsummaryrefslogtreecommitdiff
path: root/Tests/NorgKitTests/Models
diff options
context:
space:
mode:
Diffstat (limited to 'Tests/NorgKitTests/Models')
-rw-r--r--Tests/NorgKitTests/Models/DetachedModifierTests.swift61
-rw-r--r--Tests/NorgKitTests/Models/InlineLinkTests.swift32
-rw-r--r--Tests/NorgKitTests/Models/InlineSpanTests.swift35
-rw-r--r--Tests/NorgKitTests/Models/InlineStyleTests.swift35
-rw-r--r--Tests/NorgKitTests/Models/NorgBlockTests.swift127
-rw-r--r--Tests/NorgKitTests/Models/NorgDocumentTests.swift32
-rw-r--r--Tests/NorgKitTests/Models/NorgNodeTests.swift31
-rw-r--r--Tests/NorgKitTests/Models/NorgTaskTests.swift33
-rw-r--r--Tests/NorgKitTests/Models/TaskStatusTests.swift54
9 files changed, 440 insertions, 0 deletions
diff --git a/Tests/NorgKitTests/Models/DetachedModifierTests.swift b/Tests/NorgKitTests/Models/DetachedModifierTests.swift
new file mode 100644
index 0000000..7ef1479
--- /dev/null
+++ b/Tests/NorgKitTests/Models/DetachedModifierTests.swift
@@ -0,0 +1,61 @@
+import Testing
+
+@testable import NorgKit
+
+struct DetachedModifierTests {
+
+ private let markers = ASCIIByteSet("*-~>$^:")
+
+ @Test func parsesMarkerLevelAndContent() {
+ let m = DetachedModifier.parse("** heading"[...], accepting: markers)
+ #expect(m?.marker == "*")
+ #expect(m?.level == 2)
+ #expect(m?.content == "heading")
+ #expect(m?.status == nil)
+ }
+
+ @Test func skipsLeadingWhitespace() {
+ let m = DetachedModifier.parse(" - item"[...], accepting: markers)
+ #expect(m?.marker == "-")
+ #expect(m?.level == 1)
+ #expect(m?.content == "item")
+ }
+
+ @Test func parsesStatusAndStatusIndex() {
+ let line = "- (x) done"
+ let m = DetachedModifier.parse(line[...], accepting: markers)
+ #expect(m?.status == .done)
+ #expect(m?.content == "done")
+
+ if let index = m?.statusIndex {
+ #expect(line[index] == "x")
+ } else {
+ Issue.record("Expected a status index")
+ }
+ }
+
+ @Test func returnsNilForUnacceptedMarker() {
+ #expect(DetachedModifier.parse("+ item"[...], accepting: markers) == nil)
+ }
+
+ @Test func returnsNilWithoutWhitespaceAfterMarker() {
+ #expect(DetachedModifier.parse("*bold*"[...], accepting: markers) == nil)
+ }
+
+ @Test func returnsNilForEmptyAndBlankLines() {
+ #expect(DetachedModifier.parse(""[...], accepting: markers) == nil)
+ #expect(DetachedModifier.parse(" "[...], accepting: markers) == nil)
+ }
+
+ @Test func unterminatedStatusParenIsTreatedAsContent() {
+ let m = DetachedModifier.parse("- (x oops"[...], accepting: markers)
+ #expect(m?.status == nil)
+ #expect(m?.content == "(x oops")
+ }
+
+ @Test func unknownStatusCharInParensIsContent() {
+ let m = DetachedModifier.parse("- (z) text"[...], accepting: markers)
+ #expect(m?.status == nil)
+ #expect(m?.content == "(z) text")
+ }
+}
diff --git a/Tests/NorgKitTests/Models/InlineLinkTests.swift b/Tests/NorgKitTests/Models/InlineLinkTests.swift
new file mode 100644
index 0000000..9cf2cce
--- /dev/null
+++ b/Tests/NorgKitTests/Models/InlineLinkTests.swift
@@ -0,0 +1,32 @@
+import Foundation
+import Testing
+
+@testable import NorgKit
+
+struct InlineLinkTests {
+
+ @Test func storesKindAndTarget() {
+ let link = InlineLink(kind: .link, target: "https://example.com")
+ #expect(link.kind == .link)
+ #expect(link.target == "https://example.com")
+ }
+
+ @Test func anchorMayHaveNoTarget() {
+ let anchor = InlineLink(kind: .anchor, target: nil)
+ #expect(anchor.kind == .anchor)
+ #expect(anchor.target == nil)
+ }
+
+ @Test func equatableDistinguishesKindAndTarget() {
+ #expect(InlineLink(kind: .link, target: "a") == InlineLink(kind: .link, target: "a"))
+ #expect(InlineLink(kind: .link, target: "a") != InlineLink(kind: .anchor, target: "a"))
+ #expect(InlineLink(kind: .link, target: "a") != InlineLink(kind: .link, target: "b"))
+ }
+
+ @Test func roundTripsThroughJSON() throws {
+ for link in [InlineLink(kind: .link, target: "x"), InlineLink(kind: .anchor, target: nil)] {
+ let data = try JSONEncoder().encode(link)
+ #expect(try JSONDecoder().decode(InlineLink.self, from: data) == link)
+ }
+ }
+}
diff --git a/Tests/NorgKitTests/Models/InlineSpanTests.swift b/Tests/NorgKitTests/Models/InlineSpanTests.swift
new file mode 100644
index 0000000..24531e4
--- /dev/null
+++ b/Tests/NorgKitTests/Models/InlineSpanTests.swift
@@ -0,0 +1,35 @@
+import Foundation
+import Testing
+
+@testable import NorgKit
+
+struct InlineSpanTests {
+
+ @Test func defaultsToNoStyleAndNoLink() {
+ let span = InlineSpan(text: "hi")
+ #expect(span.styles.isEmpty)
+ #expect(span.link == nil)
+ }
+
+ @Test func storesStyleAndLink() {
+ let link = InlineLink(kind: .link, target: "https://example.com")
+ let span = InlineSpan(text: "hi", styles: .bold, link: link)
+ #expect(span.styles.contains(.bold))
+ #expect(span.link == link)
+ }
+
+ @Test func equatableDistinguishesFields() {
+ #expect(InlineSpan(text: "a") == InlineSpan(text: "a"))
+ #expect(InlineSpan(text: "a") != InlineSpan(text: "b"))
+ #expect(InlineSpan(text: "a", styles: .bold) != InlineSpan(text: "a"))
+ }
+
+ @Test func roundTripsThroughJSON() throws {
+ let span = InlineSpan(
+ text: "hi", styles: [.bold, .italic],
+ link: InlineLink(kind: .anchor, target: nil)
+ )
+ let data = try JSONEncoder().encode(span)
+ #expect(try JSONDecoder().decode(InlineSpan.self, from: data) == span)
+ }
+}
diff --git a/Tests/NorgKitTests/Models/InlineStyleTests.swift b/Tests/NorgKitTests/Models/InlineStyleTests.swift
new file mode 100644
index 0000000..4eafc12
--- /dev/null
+++ b/Tests/NorgKitTests/Models/InlineStyleTests.swift
@@ -0,0 +1,35 @@
+import Foundation
+import Testing
+
+@testable import NorgKit
+
+struct InlineStyleTests {
+
+ @Test func combinesAndContains() {
+ let style: InlineStyle = [.bold, .italic]
+ #expect(style.contains(.bold))
+ #expect(style.contains(.italic))
+ #expect(!style.contains(.underline))
+ }
+
+ @Test func distinctRawValues() {
+ let all: [InlineStyle] = [
+ .bold, .italic, .underline, .strikethrough,
+ .verbatim, .superscript, .subscript, .spoiler, .math,
+ ]
+ #expect(Set(all.map(\.rawValue)).count == all.count)
+ }
+
+ @Test func encodesAsSingleInteger() throws {
+ let style: InlineStyle = [.bold, .verbatim]
+ let data = try JSONEncoder().encode(style)
+ #expect(String(data: data, encoding: .utf8) == "\(style.rawValue)")
+ }
+
+ @Test func roundTripsThroughJSON() throws {
+ let style: InlineStyle = [.italic, .math, .spoiler]
+ let data = try JSONEncoder().encode(style)
+ let decoded = try JSONDecoder().decode(InlineStyle.self, from: data)
+ #expect(decoded == style)
+ }
+}
diff --git a/Tests/NorgKitTests/Models/NorgBlockTests.swift b/Tests/NorgKitTests/Models/NorgBlockTests.swift
new file mode 100644
index 0000000..d9d18c2
--- /dev/null
+++ b/Tests/NorgKitTests/Models/NorgBlockTests.swift
@@ -0,0 +1,127 @@
+import Foundation
+import Testing
+
+@testable import NorgKit
+
+struct NorgBlockTests {
+
+ private let span = [InlineSpan(text: "x")]
+ private let title = [InlineSpan(text: "t")]
+ private let body: [NorgBlock] = [.paragraph(content: [InlineSpan(text: "b")], line: 1)]
+
+ private var allBlocks: [NorgBlock] {
+ [
+ .heading(level: 1, status: .done, content: span, line: 0),
+ .paragraph(content: span, line: 1),
+ .unorderedListItem(level: 1, status: .undone, content: span, line: 2),
+ .orderedListItem(level: 1, status: .pending, content: span, line: 3),
+ .quote(level: 1, status: .urgent, content: span, line: 4),
+ .codeBlock(language: "swift", code: "c", line: 5),
+ .definition(title: title, status: .needsInput, body: body, line: 6),
+ .footnote(title: title, status: .onHold, body: body, line: 7),
+ .tableCell(title: title, status: .recurring, body: body, line: 8),
+ .rangedTag(name: ["img"], parameters: ["a"], content: "c", line: 9),
+ .horizontalRule(line: 10),
+ .weakDelimiter(line: 11),
+ .strongDelimiter(line: 12),
+ ]
+ }
+
+ @Test func lineReportsSourceLineForEveryCase() {
+ for (expected, block) in allBlocks.enumerated() {
+ #expect(block.line == expected)
+ }
+ }
+
+ @Test func statusReturnsValueForStatusBearingBlocks() {
+ #expect(NorgBlock.heading(level: 1, status: .done, content: span, line: 0).status == .done)
+ #expect(
+ NorgBlock.unorderedListItem(level: 1, status: .undone, content: span, line: 0).status
+ == .undone)
+ #expect(
+ NorgBlock.orderedListItem(level: 1, status: .pending, content: span, line: 0).status
+ == .pending)
+ #expect(NorgBlock.quote(level: 1, status: .urgent, content: span, line: 0).status == .urgent)
+ #expect(
+ NorgBlock.definition(title: title, status: .needsInput, body: body, line: 0).status
+ == .needsInput)
+ #expect(
+ NorgBlock.footnote(title: title, status: .onHold, body: body, line: 0).status == .onHold)
+ #expect(
+ NorgBlock.tableCell(title: title, status: .recurring, body: body, line: 0).status
+ == .recurring)
+ }
+
+ @Test func statusIsNilForBlocksThatCannotCarryOne() {
+ #expect(NorgBlock.paragraph(content: span, line: 0).status == nil)
+ #expect(NorgBlock.codeBlock(language: nil, code: "c", line: 0).status == nil)
+ #expect(NorgBlock.rangedTag(name: ["x"], parameters: [], content: "c", line: 0).status == nil)
+ #expect(NorgBlock.horizontalRule(line: 0).status == nil)
+ #expect(NorgBlock.weakDelimiter(line: 0).status == nil)
+ #expect(NorgBlock.strongDelimiter(line: 0).status == nil)
+ }
+
+ @Test func contentReturnsInlineSpansOrTitle() {
+ #expect(NorgBlock.heading(level: 1, status: nil, content: span, line: 0).content == span)
+ #expect(NorgBlock.paragraph(content: span, line: 0).content == span)
+ #expect(
+ NorgBlock.unorderedListItem(level: 1, status: nil, content: span, line: 0).content == span)
+ #expect(
+ NorgBlock.orderedListItem(level: 1, status: nil, content: span, line: 0).content == span)
+ #expect(NorgBlock.quote(level: 1, status: nil, content: span, line: 0).content == span)
+ #expect(NorgBlock.definition(title: title, status: nil, body: body, line: 0).content == title)
+ #expect(NorgBlock.footnote(title: title, status: nil, body: body, line: 0).content == title)
+ #expect(NorgBlock.tableCell(title: title, status: nil, body: body, line: 0).content == title)
+ }
+
+ @Test func contentIsNilForContentlessBlocks() {
+ #expect(NorgBlock.codeBlock(language: nil, code: "c", line: 0).content == nil)
+ #expect(NorgBlock.rangedTag(name: ["x"], parameters: [], content: "c", line: 0).content == nil)
+ #expect(NorgBlock.horizontalRule(line: 0).content == nil)
+ #expect(NorgBlock.weakDelimiter(line: 0).content == nil)
+ #expect(NorgBlock.strongDelimiter(line: 0).content == nil)
+ }
+
+ @Test func bodyReturnsNestedBlocksForRangeableBlocks() {
+ #expect(NorgBlock.definition(title: title, status: nil, body: body, line: 0).body == body)
+ #expect(NorgBlock.footnote(title: title, status: nil, body: body, line: 0).body == body)
+ #expect(NorgBlock.tableCell(title: title, status: nil, body: body, line: 0).body == body)
+ }
+
+ @Test func bodyIsNilForNonRangeableBlocks() {
+ #expect(NorgBlock.heading(level: 1, status: nil, content: span, line: 0).body == nil)
+ #expect(NorgBlock.paragraph(content: span, line: 0).body == nil)
+ #expect(NorgBlock.codeBlock(language: nil, code: "c", line: 0).body == nil)
+ #expect(NorgBlock.horizontalRule(line: 0).body == nil)
+ }
+
+ @Test func settingStatusReplacesStatusOnSupportingBlocks() {
+ let cases: [NorgBlock] = [
+ .heading(level: 2, status: nil, content: span, line: 0),
+ .unorderedListItem(level: 1, status: nil, content: span, line: 0),
+ .orderedListItem(level: 1, status: nil, content: span, line: 0),
+ .quote(level: 1, status: nil, content: span, line: 0),
+ .definition(title: title, status: nil, body: body, line: 0),
+ .footnote(title: title, status: nil, body: body, line: 0),
+ .tableCell(title: title, status: nil, body: body, line: 0),
+ ]
+ for block in cases {
+ #expect(block.status == nil)
+ let set = block.settingStatus(.done)
+ #expect(set.status == .done)
+ #expect(set.line == block.line)
+ #expect(set.content == block.content)
+ #expect(set.body == block.body)
+ #expect(set.settingStatus(nil).status == nil)
+ }
+ }
+
+ @Test func settingStatusLeavesUnsupportedBlocksUnchanged() {
+ let paragraph = NorgBlock.paragraph(content: span, line: 0)
+ #expect(paragraph.settingStatus(.done) == paragraph)
+ let rule = NorgBlock.horizontalRule(line: 0)
+ #expect(rule.settingStatus(.done) == rule)
+ let code = NorgBlock.codeBlock(language: "swift", code: "c", line: 0)
+ #expect(code.settingStatus(.urgent) == code)
+ }
+}
diff --git a/Tests/NorgKitTests/Models/NorgDocumentTests.swift b/Tests/NorgKitTests/Models/NorgDocumentTests.swift
new file mode 100644
index 0000000..13bad67
--- /dev/null
+++ b/Tests/NorgKitTests/Models/NorgDocumentTests.swift
@@ -0,0 +1,32 @@
+import Foundation
+import Testing
+
+@testable import NorgKit
+
+struct NorgDocumentTests {
+
+ @Test func defaultsToEmptyBlocks() {
+ #expect(NorgDocument().blocks.isEmpty)
+ }
+
+ @Test func treeFoldsBlocksIntoNodes() {
+ let doc = NorgDocument(blocks: [
+ .heading(level: 1, status: nil, content: [InlineSpan(text: "H")], line: 0),
+ .paragraph(content: [InlineSpan(text: "body")], line: 1),
+ ])
+ let tree = doc.tree()
+ #expect(tree.count == 1)
+ #expect(tree.first?.children.count == 1)
+ }
+
+ @Test func treeMatchesTreeFolderOutput() {
+ let doc = NorgParser.parse("* H\n- a\n-- b")
+ #expect(doc.tree() == TreeFolder(doc.blocks).fold())
+ }
+
+ @Test func roundTripsThroughJSON() throws {
+ let doc = NorgParser.parse("* H\nbody\n- ( ) task")
+ let data = try JSONEncoder().encode(doc)
+ #expect(try JSONDecoder().decode(NorgDocument.self, from: data) == doc)
+ }
+}
diff --git a/Tests/NorgKitTests/Models/NorgNodeTests.swift b/Tests/NorgKitTests/Models/NorgNodeTests.swift
new file mode 100644
index 0000000..5e60014
--- /dev/null
+++ b/Tests/NorgKitTests/Models/NorgNodeTests.swift
@@ -0,0 +1,31 @@
+import Foundation
+import Testing
+
+@testable import NorgKit
+
+struct NorgNodeTests {
+
+ private let block = NorgBlock.paragraph(content: [InlineSpan(text: "x")], line: 0)
+
+ @Test func defaultsToNoChildren() {
+ #expect(NorgNode(block: block).children.isEmpty)
+ }
+
+ @Test func storesBlockAndChildren() {
+ let child = NorgNode(block: block)
+ let node = NorgNode(block: block, children: [child])
+ #expect(node.block == block)
+ #expect(node.children == [child])
+ }
+
+ @Test func equatableComparesBlockAndChildren() {
+ #expect(NorgNode(block: block) == NorgNode(block: block))
+ #expect(NorgNode(block: block, children: [NorgNode(block: block)]) != NorgNode(block: block))
+ }
+
+ @Test func roundTripsThroughJSON() throws {
+ let node = NorgNode(block: block, children: [NorgNode(block: block)])
+ let data = try JSONEncoder().encode(node)
+ #expect(try JSONDecoder().decode(NorgNode.self, from: data) == node)
+ }
+}
diff --git a/Tests/NorgKitTests/Models/NorgTaskTests.swift b/Tests/NorgKitTests/Models/NorgTaskTests.swift
new file mode 100644
index 0000000..e8707a1
--- /dev/null
+++ b/Tests/NorgKitTests/Models/NorgTaskTests.swift
@@ -0,0 +1,33 @@
+import Foundation
+import Testing
+
+@testable import NorgKit
+
+struct NorgTaskTests {
+
+ private let file = URL(filePath: "/tmp/notes.norg")
+
+ @Test func idCombinesFileURLAndLine() {
+ let task = NorgTask(fileURL: file, line: 7, status: .undone, text: "do it")
+ #expect(task.id == "\(file.absoluteString):7")
+ }
+
+ @Test func idIsStableForSameFileAndLine() {
+ let a = NorgTask(fileURL: file, line: 3, status: .done, text: "a")
+ let b = NorgTask(fileURL: file, line: 3, status: .urgent, text: "b")
+ #expect(a.id == b.id)
+ }
+
+ @Test func differentLinesProduceDifferentIDs() {
+ let a = NorgTask(fileURL: file, line: 1, status: .done, text: "a")
+ let b = NorgTask(fileURL: file, line: 2, status: .done, text: "a")
+ #expect(a.id != b.id)
+ }
+
+ @Test func roundTripsThroughJSON() throws {
+ let task = NorgTask(fileURL: file, line: 4, status: .pending, text: "keep")
+ let data = try JSONEncoder().encode(task)
+ let decoded = try JSONDecoder().decode(NorgTask.self, from: data)
+ #expect(decoded == task)
+ }
+}
diff --git a/Tests/NorgKitTests/Models/TaskStatusTests.swift b/Tests/NorgKitTests/Models/TaskStatusTests.swift
new file mode 100644
index 0000000..a00e50c
--- /dev/null
+++ b/Tests/NorgKitTests/Models/TaskStatusTests.swift
@@ -0,0 +1,54 @@
+import Foundation
+import Testing
+
+@testable import NorgKit
+
+struct TaskStatusTests {
+
+ @Test func parsesEveryMarker() {
+ #expect(TaskStatus(marker: " ") == .undone)
+ #expect(TaskStatus(marker: "x") == .done)
+ #expect(TaskStatus(marker: "?") == .needsInput)
+ #expect(TaskStatus(marker: "!") == .urgent)
+ #expect(TaskStatus(marker: "+") == .recurring)
+ #expect(TaskStatus(marker: "-") == .pending)
+ #expect(TaskStatus(marker: "=") == .onHold)
+ #expect(TaskStatus(marker: "_") == .cancelled)
+ }
+
+ @Test func rejectsUnknownMarker() {
+ #expect(TaskStatus(marker: "z") == nil)
+ }
+
+ @Test func parsesEveryMarkerByte() {
+ #expect(TaskStatus(markerByte: UInt8(ascii: " ")) == .undone)
+ #expect(TaskStatus(markerByte: UInt8(ascii: "x")) == .done)
+ #expect(TaskStatus(markerByte: UInt8(ascii: "?")) == .needsInput)
+ #expect(TaskStatus(markerByte: UInt8(ascii: "!")) == .urgent)
+ #expect(TaskStatus(markerByte: UInt8(ascii: "+")) == .recurring)
+ #expect(TaskStatus(markerByte: UInt8(ascii: "-")) == .pending)
+ #expect(TaskStatus(markerByte: UInt8(ascii: "=")) == .onHold)
+ #expect(TaskStatus(markerByte: UInt8(ascii: "_")) == .cancelled)
+ }
+
+ @Test func rejectsUnknownMarkerByte() {
+ #expect(TaskStatus(markerByte: UInt8(ascii: "z")) == nil)
+ }
+
+ @Test func idEqualsRawValue() {
+ for status in TaskStatus.allCases {
+ #expect(status.id == status.rawValue)
+ }
+ }
+
+ @Test func rawValuesMatchMarkers() {
+ #expect(TaskStatus.undone.rawValue == " ")
+ #expect(TaskStatus.done.rawValue == "x")
+ #expect(TaskStatus.allCases.count == 8)
+ }
+
+ @Test func roundTripsThroughJSON() throws {
+ let data = try JSONEncoder().encode(TaskStatus.urgent)
+ #expect(try JSONDecoder().decode(TaskStatus.self, from: data) == .urgent)
+ }
+}