krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Tests/OrgCoreTests/FootnoteTests.swift

ab6ccec56eca03d0dadf2c9332aab10c4490eb62
orgstar/Tests/OrgCoreTests/FootnoteTests.swift history · blame · raw

99 lines · 4944 bytes

 1import Foundation
 2import Testing
 3@testable import OrgCore
 4
 5struct FootnoteTests {
 6    static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
 7
 8    static let messy = """
 9        #+TITLE: Notes
10        * H
11        A[fn:b] B[fn:2] C[fn::anon text] D[fn:x:inline def].
12
13        [fn:2] Two.
14        Still two.
15
16        [fn:b] Bee[fn:n].
17
18        [fn:z] Unused.
19
20        [fn:n] Nested.
21        * Other
22        E[fn:2] and *bold* text
23        """ + "\n"
24
25    /// (text, where the caret goes: the offset of this marker's end, or its start with a "^"
26    /// prefix, in the text).
27    static func caret(_ text: String, _ at: String) -> Int {
28        let start = at.hasPrefix("^")
29        let needle = start ? String(at.dropFirst()) : at
30        let r = (text as NSString).range(of: needle)
31        precondition(r.location != NSNotFound, needle)
32        return start ? r.location : NSMaxRange(r)
33    }
34
35    func compare(_ cases: [(text: String, at: String, command: any OrgCommand, answers: [String: String], form: String)]) throws {
36        let emacs = try EmacsOracle.run(cases.map { c in
37            EmacsOracle.Case(text: c.text, point: EmacsOracle.point(Self.caret(c.text, c.at), in: c.text), form: c.form)
38        })
39        for (c, theirs) in zip(cases, emacs) {
40            let ours = runCommand(c.command, c.text, caret: Self.caret(c.text, c.at), answers: c.answers)
41            let theirCaret = EmacsOracle.offset(theirs.point, in: theirs.text)
42            #expect(ours.text == theirs.text && ours.failure == nil && ours.caret == theirCaret && theirs.error.isEmpty,
43                    "\(c.form) at \(c.at.debugDescription)\nours:  \(ours.text.debugDescription) @\(ours.caret) \(ours.failure ?? "")\nemacs: \(theirs.text.debugDescription) @\(theirCaret) \(theirs.error)")
44        }
45    }
46
47    static func menu(_ key: Character) -> String {
48        "(cl-letf (((symbol-function 'read-char-exclusive) (lambda (&rest _) ?\(key)))) (org-footnote-action '(4)))"
49    }
50
51    @Test(.enabled(if: oracle))
52    func actsLikeOrgFootnoteAction() throws {
53        let action = "(cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))) (org-footnote-action))"
54        let plain = "* H\nSome text here.\n"
55        let sectioned = "* H\nText[fn:1] more.\n\n* Footnotes\n\n[fn:1] One.\n"
56        let tagged = "* TODO Heading title :tag:\nBody\n"
57        let missing = "* H\nSee[fn:q] here.\n* Next\n"
58        try compare([
59            (plain, "Some text", FootnoteAction(), [:], action),
60            (plain, "here.", FootnoteAction(), [:], action),
61            ("Top text\n", "Top", FootnoteAction(), [:], action),
62            (sectioned, "more", FootnoteAction(), [:], action),
63            (sectioned, "Text[fn", FootnoteAction(), [:], action),
64            (sectioned, "^[fn:1] One", FootnoteAction(), [:], action),
65            (sectioned, "[fn:1] O", FootnoteAction(), [:], action),
66            (tagged, "Heading", FootnoteAction(), [:], action),
67            (Self.messy, "B[fn:", FootnoteAction(), [:], action),
68            (Self.messy, "C[fn::an", FootnoteAction(), [:], action),
69            (Self.messy, "D[fn:x:in", FootnoteAction(), [:], action),
70            (Self.messy, "^[fn:b] Bee", FootnoteAction(), [:], action),
71            (Self.messy, "Still", FootnoteAction(), [:], action),
72            (Self.messy, "E[fn:2] and *bo", FootnoteAction(), [:], action),
73            (missing, "See[fn", FootnoteAction(), ["create": "yes"], action),
74        ])
75    }
76
77    @Test(.enabled(if: oracle))
78    func runsTheFootnoteMenuLikeOrg() throws {
79        let text = Self.messy
80        var cases: [(text: String, at: String, command: any OrgCommand, answers: [String: String], form: String)] = []
81        for key in ["s", "r", "S", "n"] {
82            cases.append((text, "Notes", FootnoteAction(special: true), ["footnote": key], Self.menu(Character(key))))
83        }
84        cases.append((text, "B[fn:", FootnoteAction(special: true), ["footnote": "d"], Self.menu("d")))
85        cases.append((text, "C[fn::an", FootnoteAction(special: true), ["footnote": "d"], Self.menu("d")))
86        cases.append((text, "Unused", FootnoteAction(special: true), ["footnote": "d"], Self.menu("d")))
87        let numbered = "* H\nX[fn:3] Y[fn:1] Z[fn:3]\n\n[fn:1] one\n\n[fn:3] three\n\n[fn:7] seven\n"
88        cases.append((numbered, "^X", FootnoteAction(special: true), ["footnote": "r"], Self.menu("r")))
89        cases.append((numbered, "^X", FootnoteAction(special: true), ["footnote": "S"], Self.menu("S")))
90        cases.append((numbered, "^X", FootnoteAction(special: true), ["footnote": "n"], Self.menu("n")))
91        try compare(cases)
92    }
93
94    @Test func offersTheMenuWhereNoReferenceCanGo() {
95        let text = "#+TITLE: Notes\n| a | b |\n"
96        #expect(runCommand(FootnoteAction(), text, caret: 5).failure == "prompt: footnote")
97        #expect(runCommand(CtrlCCtrlC(), "A[fn:1]\n\n[fn:1] x\n", caret: 2).caret == 15)
98    }
99}