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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
;;; test-pearl-format.el --- Tests for org entry rendering -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Craig Jennings
;; Author: Craig Jennings <c@cjennings.net>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Tests for the org renderer: `pearl--format-issue-as-org-entry' (a
;; normalized issue plist -> heading + LINEAR-* drawer + body description),
;; `pearl--description-to-org-body' (the interim heading guard), and
;; `pearl--build-org-content'. Issues come from the shared fixtures via
;; `pearl--normalize-issue', so the renderer is exercised on the same
;; shapes production hands it.
;;; Code:
(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
(require 'testutil-fixtures (expand-file-name "testutil-fixtures.el"))
(defmacro test-pearl--with-default-mapping (&rest body)
"Run BODY with the default state mapping and a clean pattern cache."
(declare (indent 0))
`(let ((pearl-state-to-todo-mapping
'(("Todo" . "TODO") ("In Progress" . "IN-PROGRESS")
("In Review" . "IN-REVIEW") ("Backlog" . "BACKLOG")
("Blocked" . "BLOCKED") ("Done" . "DONE")))
(pearl-todo-states-pattern nil)
(pearl--todo-states-pattern-source nil))
,@body))
(defun test-pearl--norm-full ()
"A normalized fully-populated issue."
(pearl--normalize-issue (testutil-linear-fixture-issue-full)))
(defun test-pearl--norm-bare ()
"A normalized issue with null/missing optional fields."
(pearl--normalize-issue (testutil-linear-fixture-issue-null-fields)))
;;; format-issue-as-org-entry
(ert-deftest test-pearl-format-issue-full-renders-heading-and-drawer ()
"A full issue renders the heading and the namespaced LINEAR-* drawer."
(test-pearl--with-default-mapping
(let ((out (pearl--format-issue-as-org-entry (test-pearl--norm-full))))
(should (string-match-p "^\\*\\* IN-PROGRESS \\[#B\\] Fix the thing$" out))
(should (string-match-p "^:LINEAR-ID: +uuid-1$" out))
(should (string-match-p "^:LINEAR-IDENTIFIER: +ENG-42$" out))
(should (string-match-p "^:LINEAR-STATE-NAME: +In Progress$" out))
(should (string-match-p "^:LINEAR-TEAM-NAME: +Engineering$" out))
(should (string-match-p "^:LINEAR-PROJECT-NAME: +Platform$" out))
(should (string-match-p "^:LINEAR-ASSIGNEE-NAME: +Craig$" out))
(should (string-match-p "^:LINEAR-LABELS: +\\[bug, backend\\]$" out))
(should (string-match-p "^:LINEAR-DESC-SHA256: +[0-9a-f]\\{64\\}$" out))
(should (string-match-p "^:LINEAR-TITLE-SHA256: +[0-9a-f]\\{64\\}$" out))
(should (string-match-p "^:END:$" out)))))
(ert-deftest test-pearl-format-issue-description-in-body-not-property ()
"The description renders as body text, not a :DESCRIPTION: property."
(test-pearl--with-default-mapping
(let ((out (pearl--format-issue-as-org-entry (test-pearl--norm-full))))
(should-not (string-match-p ":DESCRIPTION:" out))
(should (string-match-p "Line one" out))
(should (string-match-p "Line two" out)))))
(ert-deftest test-pearl-format-issue-bare-empty-optionals ()
"Null/missing optional fields render as empty values, and the body is empty."
(test-pearl--with-default-mapping
(let ((out (pearl--format-issue-as-org-entry (test-pearl--norm-bare))))
(should (string-match-p "^\\*\\* TODO \\[#C\\] Bare issue$" out))
(should (string-match-p "^:LINEAR-PROJECT-NAME: +$" out))
(should (string-match-p "^:LINEAR-ASSIGNEE-NAME: +$" out))
(should (string-match-p "^:LINEAR-LABELS: +\\[\\]$" out))
;; null description -> nothing after :END:
(should (string-match-p ":END:\n\\'" out)))))
(ert-deftest test-pearl-format-issue-strips-brackets-from-title ()
"Square brackets in the title are stripped so org parsing stays sane."
(test-pearl--with-default-mapping
(let ((out (pearl--format-issue-as-org-entry
'(:id "u" :identifier "ENG-1" :title "Fix [URGENT] bug"
:priority 1 :state (:name "Todo")))))
(should (string-match-p "^\\*\\* TODO \\[#A\\] Fix URGENT bug$" out))
;; the title provenance hash is of the stripped (rendered) title, so a
;; later no-op title sync matches the heading and never clobbers brackets
(should (string-match-p
(format "^:LINEAR-TITLE-SHA256: +%s$" (secure-hash 'sha256 "Fix URGENT bug"))
out)))))
;;; build-org-content
(ert-deftest test-pearl-build-org-content-empty-issues-header-only ()
"With no issues the content is the file header plus the empty parent, no entries."
(test-pearl--with-default-mapping
(let ((out (pearl--build-org-content '())))
(should (string-match-p "^#\\+title:" out))
(should-not (string-match-p "^\\*\\* " out)))))
(ert-deftest test-pearl-build-org-content-no-hardcoded-filetags ()
"The header carries no hardcoded =#+filetags= (a personal value used to leak in)."
(test-pearl--with-default-mapping
(let ((out (pearl--build-org-content '())))
(should-not (string-match-p "#\\+filetags" out))
(should-not (string-match-p "twai" out)))))
(ert-deftest test-pearl-build-org-content-renders-view-parent-heading ()
"Issues nest under a single top-level heading named after the view, so they
sort together (org-sort on the parent) instead of being orphan headings."
(test-pearl--with-default-mapping
(let ((out (pearl--build-org-content
(list (test-pearl--norm-full))
'(:type filter :name "My open issues" :filter nil))))
(should (string-match-p "^\\* My open issues$" out))
;; the parent precedes the issue, which renders one level deeper
(should (< (string-match "^\\* My open issues$" out)
(string-match "^\\*\\* IN-PROGRESS" out))))))
(ert-deftest test-pearl-build-org-content-startup-show3levels ()
"The page opens folded to headings (parent, issues, Comments), bodies hidden."
(test-pearl--with-default-mapping
(let ((out (pearl--build-org-content '())))
(should (string-match-p "^#\\+STARTUP: show3levels$" out))
(should-not (string-match-p "^#\\+STARTUP: overview$" out)))))
(ert-deftest test-pearl-build-org-content-no-shared-file-id ()
"The file header carries no hardcoded org :ID: drawer."
(test-pearl--with-default-mapping
(let ((out (pearl--build-org-content '())))
(should-not (string-match-p "a12acb12" out))
(should-not (string-match-p "^:PROPERTIES:$" out)))))
(ert-deftest test-pearl-build-org-content-includes-each-issue ()
"Each issue contributes one heading to the rendered content."
(test-pearl--with-default-mapping
(let ((out (pearl--build-org-content
(list (test-pearl--norm-full) (test-pearl--norm-bare)))))
(should (string-match-p "^\\*\\* IN-PROGRESS \\[#B\\] Fix the thing$" out))
(should (string-match-p "^\\*\\* TODO \\[#C\\] Bare issue$" out)))))
;;; --restore-page-visibility
(defun test-pearl--line-visible-p (re)
"Non-nil when the line matching RE from point-min is not folded away."
(save-excursion
(goto-char (point-min))
(and (re-search-forward re nil t)
(not (invisible-p (line-beginning-position))))))
(ert-deftest test-pearl-restore-page-visibility-folds-bodies-keeps-headings ()
"After a repopulation the page folds to headings: parent and issues stay
visible while property drawers fold away."
(test-pearl--with-default-mapping
(let ((pearl-fold-after-update t))
(with-temp-buffer
(insert (pearl--build-org-content (list (test-pearl--norm-full))))
(org-mode)
(org-fold-show-all)
(pearl--restore-page-visibility)
(should (test-pearl--line-visible-p "^\\* "))
(should (test-pearl--line-visible-p "^\\*\\* IN-PROGRESS"))
(should-not (test-pearl--line-visible-p "^:LINEAR-ID:"))))))
(ert-deftest test-pearl-restore-page-visibility-noop-when-disabled ()
"With `pearl-fold-after-update' nil the buffer is left fully expanded."
(test-pearl--with-default-mapping
(let ((pearl-fold-after-update nil))
(with-temp-buffer
(insert (pearl--build-org-content (list (test-pearl--norm-full))))
(org-mode)
(org-fold-show-all)
(pearl--restore-page-visibility)
(should (test-pearl--line-visible-p "^:LINEAR-ID:"))))))
(provide 'test-pearl-format)
;;; test-pearl-format.el ends here
|