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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
|
;;; test-org-capture-config-popup-window.el --- Quick-capture popup single-window tests -*- lexical-binding: t; -*-
;;; Commentary:
;; Tests for the pure predicate behind the quick-capture popup single-window
;; fix. The Hyprland Super+Shift+N popup opens an emacsclient frame named
;; "org-capture"; in that frame the *Org Select* template menu and the
;; CAPTURE-* buffer must fill the frame's sole window instead of splitting it.
;; `cj/org-capture--popup-sole-window-p' is the frame+buffer decision; the
;; display-buffer action that acts on it is exercised by hand (window ops),
;; not here.
;;; Code:
(require 'ert)
(require 'cl-lib)
(require 'org)
(require 'org-capture) ; makes `org-capture-templates' a real special var
(require 'user-constants)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'org-capture-config)
(defconst test-org-capture-popup--sample-templates
'(("t" "Task" entry (function cj/--org-capture-project-location)
"* TODO %?" :prepend t)
("b" "Bug" entry (function cj/--org-capture-project-location)
"* TODO [#C] %?" :prepend t)
("e" "Event" entry (file+headline schedule-file "Scheduled Events")
"* %?" :prepend t :prepare-finalize cj/org-capture-format-event-headline)
("m" "Mu4e Email" entry (file+headline inbox-file "Inbox") "* TODO %?" :prepend t)
("L" "Link" entry (file+headline inbox-file "Inbox") "* %?" :immediate-finish t)
("d" "Drill Question" entry (file ignore) "* Item :drill:\n%?" :prepend t))
"A representative org-capture-templates list for popup-subset tests.")
;;; cj/org-capture--popup-sole-window-p
(ert-deftest test-org-capture-config-popup-sole-window-p-select-menu ()
"Normal: the *Org Select* menu in the popup frame wants the sole window."
(should (cj/org-capture--popup-sole-window-p "org-capture" "*Org Select*")))
(ert-deftest test-org-capture-config-popup-sole-window-p-capture-buffer ()
"Normal: a CAPTURE-* buffer in the popup frame wants the sole window."
(should (cj/org-capture--popup-sole-window-p "org-capture" "CAPTURE-todo.org")))
(ert-deftest test-org-capture-config-popup-sole-window-p-capture-prefix-only ()
"Boundary: the bare \"CAPTURE-\" prefix still matches."
(should (cj/org-capture--popup-sole-window-p "org-capture" "CAPTURE-")))
(ert-deftest test-org-capture-config-popup-sole-window-p-other-frame ()
"Boundary: the same menu in a normal frame is left alone."
(should-not (cj/org-capture--popup-sole-window-p "emacs" "*Org Select*"))
(should-not (cj/org-capture--popup-sole-window-p nil "CAPTURE-todo.org")))
(ert-deftest test-org-capture-config-popup-sole-window-p-other-buffer ()
"Boundary: an unrelated buffer in the popup frame is left alone."
(should-not (cj/org-capture--popup-sole-window-p "org-capture" "todo.org"))
(should-not (cj/org-capture--popup-sole-window-p "org-capture" "*scratch*")))
(ert-deftest test-org-capture-config-popup-sole-window-p-nil-buffer ()
"Error: a nil or non-string buffer name returns nil without raising."
(should-not (cj/org-capture--popup-sole-window-p "org-capture" nil))
(should-not (cj/org-capture--popup-sole-window-p "org-capture" 42)))
;;; Integration: the display-buffer-alist entry routes to a sole window
(ert-deftest test-integration-org-capture-popup-display-sole-window ()
"Integration: in an \"org-capture\"-named frame, displaying a CAPTURE-*
buffer fills the frame's sole window via the registered display-buffer-alist
entry, instead of splitting.
Components integrated:
- cj/org-capture--popup-display-condition (real)
- cj/org-capture--display-sole-window (real)
- display-buffer / display-buffer-alist (real)
Validates the popup frame ends with one window showing the CAPTURE buffer."
;; The batch frame is auto-named (\"F1\"), which cannot be restored by name
;; (\"F<num> usurped by Emacs\"); reset to nil to return it to auto-naming,
;; keeping the test independent of execution order.
(let ((buf (get-buffer-create "CAPTURE-itest")))
(unwind-protect
(progn
(set-frame-parameter nil 'name "org-capture")
(delete-other-windows)
(display-buffer buf)
(should (= (length (window-list)) 1))
(should (eq (window-buffer (selected-window)) buf)))
(set-frame-parameter nil 'name nil)
(when (buffer-live-p buf) (kill-buffer buf)))))
;;; cj/--org-capture-popup-templates (pure subset/retarget)
(ert-deftest test-org-capture-config-popup-templates-keeps-tbe ()
"Normal: only Task, Bug, Event survive, preserving order."
(should (equal (mapcar #'car (cj/--org-capture-popup-templates
test-org-capture-popup--sample-templates "/inbox.org"))
'("t" "b" "e"))))
(ert-deftest test-org-capture-config-popup-templates-retargets-task-bug ()
"Normal: Task and Bug retarget to the inbox \"Inbox\" headline; body + props kept."
(let* ((result (cj/--org-capture-popup-templates
test-org-capture-popup--sample-templates "/inbox.org"))
(task (assoc "t" result))
(bug (assoc "b" result)))
(should (equal (nth 3 task) '(file+headline "/inbox.org" "Inbox")))
(should (equal (nth 3 bug) '(file+headline "/inbox.org" "Inbox")))
(should (equal (nth 4 task) "* TODO %?"))
(should (equal (nth 4 bug) "* TODO [#C] %?"))
(should (memq :prepend task))))
(ert-deftest test-org-capture-config-popup-templates-event-unchanged ()
"Boundary: Event passes through untouched, schedule-file target and props intact."
(let ((event (assoc "e" (cj/--org-capture-popup-templates
test-org-capture-popup--sample-templates "/inbox.org"))))
(should (equal (nth 3 event) '(file+headline schedule-file "Scheduled Events")))
(should (memq :prepare-finalize event))))
(ert-deftest test-org-capture-config-popup-templates-drops-context-templates ()
"Boundary: context-dependent templates (mu4e, link, drill) are dropped."
(let ((result (cj/--org-capture-popup-templates
test-org-capture-popup--sample-templates "/inbox.org")))
(should-not (assoc "m" result))
(should-not (assoc "L" result))
(should-not (assoc "d" result))))
(ert-deftest test-org-capture-config-popup-templates-empty ()
"Error/Boundary: empty or all-dropped input yields nil without raising."
(should-not (cj/--org-capture-popup-templates nil "/inbox.org"))
(should-not (cj/--org-capture-popup-templates
'(("L" "Link" entry (file+headline f "Inbox") "* %?")) "/inbox.org")))
;;; cj/quick-capture (binds the subset; integration with a stubbed org-capture)
(ert-deftest test-integration-org-capture-quick-capture-binds-subset ()
"Integration: cj/quick-capture runs org-capture with only Task/Bug/Event,
Task and Bug retargeted to the inbox.
Components integrated:
- cj/quick-capture (real)
- cj/--org-capture-popup-templates (real)
- org-capture (MOCKED — records the dynamically-bound templates)"
(let ((org-capture-templates test-org-capture-popup--sample-templates)
captured)
(cl-letf (((symbol-function 'org-capture)
(lambda (&rest _) (setq captured org-capture-templates))))
(cj/quick-capture))
(should (equal (mapcar #'car captured) '("t" "b" "e")))
(should (equal (nth 3 (assoc "t" captured)) (list 'file+headline inbox-file "Inbox")))
(should (equal (nth 3 (assoc "b" captured)) (list 'file+headline inbox-file "Inbox")))))
(ert-deftest test-integration-org-capture-quick-capture-closes-frame-on-abort ()
"Integration: when selection aborts (org-capture signals), cj/quick-capture
deletes the popup frame instead of leaving it orphaned.
Components integrated:
- cj/quick-capture (real)
- org-capture (MOCKED — signals user-error \"Abort\")
- cj/org-capture--delete-popup-frame (MOCKED — records the call)"
(let ((org-capture-templates test-org-capture-popup--sample-templates)
(deleted 0))
(cl-letf (((symbol-function 'org-capture)
(lambda (&rest _) (user-error "Abort")))
((symbol-function 'cj/org-capture--delete-popup-frame)
(lambda () (cl-incf deleted))))
(cj/quick-capture))
(should (= deleted 1))))
(ert-deftest test-integration-org-capture-quick-capture-closes-frame-on-quit ()
"Integration: a C-g (quit) during capture also closes the popup frame."
(let ((org-capture-templates test-org-capture-popup--sample-templates)
(deleted 0))
(cl-letf (((symbol-function 'org-capture)
(lambda (&rest _) (signal 'quit nil)))
((symbol-function 'cj/org-capture--delete-popup-frame)
(lambda () (cl-incf deleted))))
(cj/quick-capture))
(should (= deleted 1))))
(ert-deftest test-integration-org-capture-quick-capture-keeps-frame-on-success ()
"Integration: a successful capture (no signal) does NOT delete the frame —
the finalize hook owns that."
(let ((org-capture-templates test-org-capture-popup--sample-templates)
(deleted 0))
(cl-letf (((symbol-function 'org-capture) (lambda (&rest _) nil))
((symbol-function 'cj/org-capture--delete-popup-frame)
(lambda () (cl-incf deleted))))
(cj/quick-capture))
(should (= deleted 0))))
;;; cj/--org-capture-popup-strip-specials (drop the Customize menu entry)
(ert-deftest test-org-capture-config-popup-strip-specials-removes-customize ()
"Normal: the \"C\" Customize entry is removed, \"q\" Abort kept, order intact."
(should (equal (cj/--org-capture-popup-strip-specials
'(("C" "Customize org-capture-templates") ("q" "Abort")))
'(("q" "Abort")))))
(ert-deftest test-org-capture-config-popup-strip-specials-no-customize ()
"Boundary: specials without a \"C\" entry pass through unchanged."
(should (equal (cj/--org-capture-popup-strip-specials '(("q" "Abort")))
'(("q" "Abort")))))
(ert-deftest test-org-capture-config-popup-strip-specials-empty ()
"Error/Boundary: nil specials yields nil without raising."
(should-not (cj/--org-capture-popup-strip-specials nil)))
;;; cj/org-capture--popup-frame-p
(ert-deftest test-org-capture-config-popup-frame-p ()
"Normal/Boundary: true only when the selected frame is named \"org-capture\"."
(cl-letf (((symbol-function 'frame-parameter) (lambda (&rest _) "org-capture")))
(should (cj/org-capture--popup-frame-p)))
(cl-letf (((symbol-function 'frame-parameter) (lambda (&rest _) "emacs")))
(should-not (cj/org-capture--popup-frame-p))))
;;; cj/org-capture--popup-mks-advice (frame-gated specials stripping)
(ert-deftest test-org-capture-config-popup-mks-advice-strips-in-popup ()
"Integration: in the popup frame, org-mks receives specials without \"C\"."
(let (seen)
(cl-letf (((symbol-function 'cj/org-capture--popup-frame-p) (lambda () t)))
(cj/org-capture--popup-mks-advice
(lambda (_table _title _prompt specials) (setq seen specials))
nil nil nil '(("C" "Customize org-capture-templates") ("q" "Abort"))))
(should (equal seen '(("q" "Abort"))))))
(ert-deftest test-org-capture-config-popup-mks-advice-keeps-elsewhere ()
"Integration: in a normal frame, org-mks receives the specials untouched."
(let (seen)
(cl-letf (((symbol-function 'cj/org-capture--popup-frame-p) (lambda () nil)))
(cj/org-capture--popup-mks-advice
(lambda (_table _title _prompt specials) (setq seen specials))
nil nil nil '(("C" "Customize org-capture-templates") ("q" "Abort"))))
(should (equal seen '(("C" "Customize org-capture-templates") ("q" "Abort"))))))
;;; cj/org-capture--popup-frame (find the popup frame by name)
(ert-deftest test-org-capture-config-popup-frame-found ()
"Normal: returns the live frame whose name is \"org-capture\"."
(cl-letf (((symbol-function 'frame-list) (lambda () '(fa fb fc)))
((symbol-function 'frame-live-p) (lambda (_f) t))
((symbol-function 'frame-parameter)
(lambda (f _p) (if (eq f 'fb) "org-capture" "other"))))
(should (eq (cj/org-capture--popup-frame) 'fb))))
(ert-deftest test-org-capture-config-popup-frame-none ()
"Boundary: no popup frame present yields nil."
(cl-letf (((symbol-function 'frame-list) (lambda () '(fa fc)))
((symbol-function 'frame-live-p) (lambda (_f) t))
((symbol-function 'frame-parameter) (lambda (_f _p) "other")))
(should-not (cj/org-capture--popup-frame))))
;;; cj/quick-capture targets the popup frame
(ert-deftest test-integration-org-capture-quick-capture-selects-named-frame ()
"Integration: cj/quick-capture selects the \"org-capture\" frame found by name,
not whatever frame happens to be selected (the emacsclient -c focus race)."
(let ((org-capture-templates test-org-capture-popup--sample-templates)
(focused nil))
(cl-letf (((symbol-function 'cj/org-capture--popup-frame) (lambda () 'popup-frame))
((symbol-function 'select-frame-set-input-focus)
(lambda (f) (setq focused f)))
((symbol-function 'org-capture) (lambda (&rest _) nil)))
(cj/quick-capture))
(should (eq focused 'popup-frame))))
(ert-deftest test-integration-org-capture-quick-capture-no-frame-still-captures ()
"Integration: when no popup frame is found, cj/quick-capture skips the focus
call and still runs the capture (no error)."
(let ((org-capture-templates test-org-capture-popup--sample-templates)
(focused 'unset)
(captured nil))
(cl-letf (((symbol-function 'cj/org-capture--popup-frame) (lambda () nil))
((symbol-function 'select-frame-set-input-focus)
(lambda (f) (setq focused f)))
((symbol-function 'org-capture) (lambda (&rest _) (setq captured t))))
(cj/quick-capture))
(should (eq focused 'unset))
(should captured)))
(provide 'test-org-capture-config-popup-window)
;;; test-org-capture-config-popup-window.el ends here
|