aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-pearl-modified.el175
1 files changed, 175 insertions, 0 deletions
diff --git a/tests/test-pearl-modified.el b/tests/test-pearl-modified.el
new file mode 100644
index 0000000..cfef923
--- /dev/null
+++ b/tests/test-pearl-modified.el
@@ -0,0 +1,175 @@
+;;; test-pearl-modified.el --- Tests for the modified-ticket indicator -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+
+;; Tests for the modified-ticket indicator (docs/modified-ticket-indicator-spec.org):
+;; phase 1 covers the pushable-ticket count and its mode-line segment. The count
+;; is "tickets, not fields": each ticket with pushable dirty work counts once.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'cl-lib)
+
+(defun test-pearl-modified--scan (&rest dirtys)
+ "Build a scan list ((MARKER . DIRTY) ...) from DIRTY plists (marker unused)."
+ (mapcar (lambda (d) (cons nil d)) dirtys))
+
+;;; pushable-ticket count
+
+(ert-deftest test-pearl-count-pushable-field-dirty-ticket-counts ()
+ "A ticket with any dirty non-comment field counts, regardless of the viewer."
+ (let ((scan (test-pearl-modified--scan '(:title t) '(:description t) '(:state t))))
+ (should (= 3 (pearl--count-pushable-tickets scan "v")))
+ (should (= 3 (pearl--count-pushable-tickets scan nil)))))
+
+(ert-deftest test-pearl-count-pushable-many-fields-one-ticket-counts-once ()
+ "A ticket dirty in several fields counts once, not per field."
+ (let ((scan (test-pearl-modified--scan '(:title t :description t :labels t))))
+ (should (= 1 (pearl--count-pushable-tickets scan "v")))))
+
+(ert-deftest test-pearl-count-pushable-own-comment-only-counts ()
+ "A ticket dirty only via an own comment (author = viewer) counts."
+ (let ((scan (test-pearl-modified--scan
+ '(:comment-candidates ((:author-id "v"))))))
+ (should (= 1 (pearl--count-pushable-tickets scan "v")))))
+
+(ert-deftest test-pearl-count-pushable-readonly-comment-only-excluded ()
+ "A ticket dirty only via a non-own comment does not count."
+ (let ((scan (test-pearl-modified--scan
+ '(:comment-candidates ((:author-id "someone-else"))))))
+ (should (= 0 (pearl--count-pushable-tickets scan "v")))))
+
+(ert-deftest test-pearl-count-pushable-comment-only-unknown-viewer-excluded ()
+ "Comment-only dirt doesn't count while the viewer is unresolved or failed."
+ (let ((scan (test-pearl-modified--scan
+ '(:comment-candidates ((:author-id "v"))))))
+ (should (= 0 (pearl--count-pushable-tickets scan nil)))
+ (should (= 0 (pearl--count-pushable-tickets scan "v" t))))) ; viewer-failed
+
+(ert-deftest test-pearl-count-pushable-mixed ()
+ "A field-dirty ticket counts even when another ticket is only read-only-dirty."
+ (let ((scan (test-pearl-modified--scan
+ '(:priority t)
+ '(:comment-candidates ((:author-id "other"))))))
+ (should (= 1 (pearl--count-pushable-tickets scan "v")))))
+
+;;; mode-line segment
+
+(ert-deftest test-pearl-mode-line-appends-changed-count-with-glyph ()
+ "The lighter appends \"N <glyph> changed\" and keeps the account name."
+ (let ((pearl-accounts '(("work" :api-key "k")))
+ (pearl-active-account "work")
+ (pearl-show-modified-indicator t)
+ (pearl-ticket-glyph "🎫")
+ (pearl--modified-count 3))
+ (let ((s (pearl--mode-line-lighter)))
+ (should (string-match-p "Pearl\\[work\\]" s))
+ (should (string-match-p "3" s))
+ (should (string-match-p "🎫" s))
+ (should (string-match-p "changed" s)))))
+
+(ert-deftest test-pearl-mode-line-changed-count-falls-back-to-word ()
+ "With the glyph unset, the segment reads \"N tickets changed\"."
+ (let ((pearl-accounts '(("work" :api-key "k")))
+ (pearl-active-account "work")
+ (pearl-show-modified-indicator t)
+ (pearl-ticket-glyph "")
+ (pearl--modified-count 2))
+ (should (string-match-p "2 tickets changed" (pearl--mode-line-lighter)))))
+
+(ert-deftest test-pearl-mode-line-no-segment-when-clean ()
+ "A zero count adds no segment; the account name still shows."
+ (let ((pearl-accounts '(("work" :api-key "k")))
+ (pearl-active-account "work")
+ (pearl-show-modified-indicator t)
+ (pearl--modified-count 0))
+ (let ((s (pearl--mode-line-lighter)))
+ (should (string-match-p "Pearl\\[work\\]" s))
+ (should-not (string-match-p "changed" s)))))
+
+(ert-deftest test-pearl-mode-line-indicator-off-suppresses-count ()
+ "With the indicator disabled, no count shows even when dirty."
+ (let ((pearl-accounts nil)
+ (pearl-active-account nil)
+ (pearl-show-modified-indicator nil)
+ (pearl--modified-count 5))
+ (should-not (string-match-p "changed" (pearl--mode-line-lighter)))))
+
+;;; phase 2 -- field-region highlight
+
+(ert-deftest test-pearl-modified-regions-clean-is-nil ()
+ "A clean ticket highlights nothing."
+ (should (null (pearl--modified-highlight-regions '() "v"))))
+
+(ert-deftest test-pearl-modified-regions-title-lights-heading ()
+ (should (equal '(heading) (pearl--modified-highlight-regions '(:title t) "v"))))
+
+(ert-deftest test-pearl-modified-regions-description-lights-both ()
+ "A dirty description lights the heading (bubble-up) and the body."
+ (should (equal '(heading description)
+ (pearl--modified-highlight-regions '(:description t) "v"))))
+
+(ert-deftest test-pearl-modified-regions-own-comment-lights-heading ()
+ (should (equal '(heading)
+ (pearl--modified-highlight-regions
+ '(:comment-candidates ((:author-id "v"))) "v"))))
+
+(ert-deftest test-pearl-modified-regions-readonly-comment-nothing ()
+ "A non-own comment does not light the heading."
+ (should (null (pearl--modified-highlight-regions
+ '(:comment-candidates ((:author-id "other"))) "v"))))
+
+(ert-deftest test-pearl-modified-regions-comment-unknown-viewer-nothing ()
+ "A comment-only ticket lights nothing while the viewer is unresolved."
+ (should (null (pearl--modified-highlight-regions
+ '(:comment-candidates ((:author-id "v"))) nil))))
+
+(ert-deftest test-pearl-issue-description-region-spans-body-only ()
+ "The description region covers the body, not the drawer or the Comments child."
+ (with-temp-buffer
+ (insert "* F\n** TODO PEARL-1 Alpha\n:PROPERTIES:\n:LINEAR-ID: i1\n:END:\n"
+ "Body one.\nBody two.\n*** Comments\n**** c\nhi\n")
+ (org-mode)
+ (goto-char (point-min)) (re-search-forward "PEARL-1") (org-back-to-heading t)
+ (let* ((r (pearl--issue-description-region))
+ (text (buffer-substring-no-properties (car r) (cdr r))))
+ (should (string-match-p "Body one" text))
+ (should (string-match-p "Body two" text))
+ (should-not (string-match-p "Comments" text))
+ (should-not (string-match-p "LINEAR-ID" text)))))
+
+(ert-deftest test-pearl-apply-modified-highlights-marks-dirty-heading-idempotently ()
+ "A title-dirty issue gets exactly one heading overlay, even on a second pass."
+ (with-temp-buffer
+ (insert "* F\n** TODO PEARL-1 Alpha\n:PROPERTIES:\n:LINEAR-ID: i1\n"
+ ":LINEAR-TITLE-SHA256: deadbeef\n:END:\n")
+ (org-mode)
+ (let ((pearl-show-modified-indicator t))
+ (cl-flet ((modified-ovs ()
+ (seq-filter (lambda (o) (overlay-get o 'pearl-modified))
+ (overlays-in (point-min) (point-max)))))
+ (pearl--apply-modified-highlights)
+ (should (= 1 (length (modified-ovs))))
+ (pearl--apply-modified-highlights)
+ (should (= 1 (length (modified-ovs))))))))
+
+(ert-deftest test-pearl-apply-modified-highlights-off-clears ()
+ "With the indicator disabled, the applier lays nothing (and clears prior)."
+ (with-temp-buffer
+ (insert "* F\n** TODO PEARL-1 Alpha\n:PROPERTIES:\n:LINEAR-ID: i1\n"
+ ":LINEAR-TITLE-SHA256: deadbeef\n:END:\n")
+ (org-mode)
+ (let ((pearl-show-modified-indicator t))
+ (pearl--apply-modified-highlights))
+ (let ((pearl-show-modified-indicator nil))
+ (pearl--apply-modified-highlights))
+ (should (null (seq-filter (lambda (o) (overlay-get o 'pearl-modified))
+ (overlays-in (point-min) (point-max)))))))
+
+(provide 'test-pearl-modified)
+;;; test-pearl-modified.el ends here