Tests/OrgCoreTests/FootnoteTests.swift
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}