From 501fdce29e4d2c46c4708ad7f44056878e0db3fa Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Tue, 16 Jun 2026 12:08:21 +0200 Subject: Initial extraction from Norganize --- .../Models/DetachedModifierTests.swift | 61 ++++++++++ Tests/NorgKitTests/Models/InlineLinkTests.swift | 32 ++++++ Tests/NorgKitTests/Models/InlineSpanTests.swift | 35 ++++++ Tests/NorgKitTests/Models/InlineStyleTests.swift | 35 ++++++ Tests/NorgKitTests/Models/NorgBlockTests.swift | 127 +++++++++++++++++++++ Tests/NorgKitTests/Models/NorgDocumentTests.swift | 32 ++++++ Tests/NorgKitTests/Models/NorgNodeTests.swift | 31 +++++ Tests/NorgKitTests/Models/NorgTaskTests.swift | 33 ++++++ Tests/NorgKitTests/Models/TaskStatusTests.swift | 54 +++++++++ 9 files changed, 440 insertions(+) create mode 100644 Tests/NorgKitTests/Models/DetachedModifierTests.swift create mode 100644 Tests/NorgKitTests/Models/InlineLinkTests.swift create mode 100644 Tests/NorgKitTests/Models/InlineSpanTests.swift create mode 100644 Tests/NorgKitTests/Models/InlineStyleTests.swift create mode 100644 Tests/NorgKitTests/Models/NorgBlockTests.swift create mode 100644 Tests/NorgKitTests/Models/NorgDocumentTests.swift create mode 100644 Tests/NorgKitTests/Models/NorgNodeTests.swift create mode 100644 Tests/NorgKitTests/Models/NorgTaskTests.swift create mode 100644 Tests/NorgKitTests/Models/TaskStatusTests.swift (limited to 'Tests/NorgKitTests/Models') 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) + } +} -- cgit