;;; test-pearl-compose.el --- Tests for the compose buffer -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Craig Jennings ;; Author: Craig Jennings ;; 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 . ;;; Commentary: ;; Tests for the shared compose buffer (`pearl--compose-in-buffer' and its ;; submit/abort/body helpers) and the two commands wired onto it: ;; `pearl-add-comment' (interactive composer path) and ;; `pearl-compose-current-description'. The body is extracted below a read-only ;; header and converted Org->Markdown by the callers. ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) (require 'cl-lib) (defmacro test-pearl--with-compose (initial &rest body) "Open a compose buffer holding INITIAL, run BODY in it, capturing the submit. Binds `captured' to the value the on-finish receives (or `:none'). Stubs `pop-to-buffer' so the helper does not touch windows in batch." (declare (indent 1)) `(let ((captured :none)) (cl-letf (((symbol-function 'pop-to-buffer) (lambda (b &rest _) b))) (let ((buf (pearl--compose-in-buffer "test" "# header line\n" ,initial (lambda (text) (setq captured text))))) (with-current-buffer buf ,@body) (when (buffer-live-p buf) (kill-buffer buf)))))) ;;; --compose-in-buffer setup (ert-deftest test-pearl-compose-sets-up-org-buffer () "The compose buffer is an Org buffer with the header and the initial body." (test-pearl--with-compose "draft text" (should (eq major-mode 'org-mode)) (should (markerp pearl--compose-body-start)) (should (string-match-p "# header line" (buffer-string))) (should (string= "draft text" (pearl--compose-body))))) (ert-deftest test-pearl-compose-body-excludes-header () "The body is only the text below the read-only header." (test-pearl--with-compose "" (goto-char (point-max)) (insert "the new body") (should (string= "the new body" (pearl--compose-body))))) (ert-deftest test-pearl-compose-header-is-read-only () "Editing inside the header is refused." (test-pearl--with-compose "body" (goto-char (+ (point-min) 3)) (should-error (insert "x") :type 'text-read-only))) ;;; submit / abort (ert-deftest test-pearl-compose-submit-hands-body-to-callback () "C-c C-c hands the trimmed body to the callback and kills the buffer." (let ((captured :none) buf) (cl-letf (((symbol-function 'pop-to-buffer) (lambda (b &rest _) b))) (setq buf (pearl--compose-in-buffer "t" "# h\n" " hello " (lambda (text) (setq captured text)))) (with-current-buffer buf (pearl--compose-submit)) (should (string= "hello" captured)) (should-not (buffer-live-p buf))))) (ert-deftest test-pearl-compose-abort-skips-callback () "C-c C-k kills the buffer without invoking the callback." (let ((called nil) buf) (cl-letf (((symbol-function 'pop-to-buffer) (lambda (b &rest _) b))) (setq buf (pearl--compose-in-buffer "t" "# h\n" "body" (lambda (_) (setq called t)))) (with-current-buffer buf (pearl--compose-abort)) (should-not called) (should-not (buffer-live-p buf))))) ;;; add-comment composer wiring (defmacro test-pearl--in-issue (&rest body) "Run BODY in an org buffer with point inside a Linear issue subtree." (declare (indent 0)) `(let ((pearl-state-to-todo-mapping '(("Todo" . "TODO")))) (with-temp-buffer (insert "** TODO [#B] ENG-1: Issue\n:PROPERTIES:\n:LINEAR-ID: a\n:END:\nBody.\n") (org-mode) (goto-char (point-min)) (re-search-forward "Body\\.") ,@body))) (ert-deftest test-pearl-add-comment-interactive-composes-and-converts () "An interactive add-comment composes Org and sends the Markdown conversion." (test-pearl--in-issue (let (sent) (cl-letf (((symbol-function 'pearl--compose-in-buffer) (lambda (_label _instr initial on-finish) ;; the composer would start empty for a new comment (should (string= "" initial)) (funcall on-finish "see *bold* and `code`"))) ((symbol-function 'pearl--create-comment-async) (lambda (_id body _cb) (setq sent body)))) (pearl-add-comment) ; interactive form: body nil ;; Org markup converted to Markdown before sending (should (string= "see **bold** and `code`" sent)))))) (ert-deftest test-pearl-add-comment-with-body-skips-composer () "A non-interactive add-comment with BODY sends it directly, no composer." (test-pearl--in-issue (let ((composed nil) sent) (cl-letf (((symbol-function 'pearl--compose-in-buffer) (lambda (&rest _) (setq composed t))) ((symbol-function 'pearl--create-comment-async) (lambda (_id body _cb) (setq sent body)))) (pearl-add-comment "literal body") (should-not composed) (should (string= "literal body" sent)))))) ;;; compose-current-description wiring (ert-deftest test-pearl-compose-description-seeds-body-and-syncs () "The description composer seeds the current body and, on submit, writes it back and syncs." (test-pearl--in-issue (let (seeded synced) (cl-letf (((symbol-function 'pearl--compose-in-buffer) (lambda (_label _instr initial on-finish) (setq seeded initial) (funcall on-finish "Edited description"))) ((symbol-function 'pearl-sync-current-issue) (lambda () (setq synced t)))) (pearl-compose-current-description) ;; seeded with the existing body (should (string= "Body." seeded)) ;; the edited text was written into the issue body, then synced (should synced) (goto-char (point-min)) (should (re-search-forward "Edited description" nil t)))))) (provide 'test-pearl-compose) ;;; test-pearl-compose.el ends here