aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-06 15:11:15 -0500
committerCraig Jennings <c@cjennings.net>2026-06-06 15:11:15 -0500
commit690faf7538bb7fcd47367b4801923beea40a0ff1 (patch)
treeffc2f2b6deb1be447582981f3935e9941d732ad4 /tests
parent0a3794ccfc56045a2e4155538c145021447d09e3 (diff)
downloadpearl-690faf7538bb7fcd47367b4801923beea40a0ff1.tar.gz
pearl-690faf7538bb7fcd47367b4801923beea40a0ff1.zip
feat(views): regroup issue subtrees in place, preserving edits
Phase 2 of the interactive-grouping spec: the buffer transform, still no command. pearl--regroup-issue-subtrees re-lays-out the active view's issues by an issueGrouping value, or flattens them when given nil. It moves each issue subtree byte-for-byte and shifts only its heading stars, so descriptions, comments, and unsaved edits ride along untouched. The group label comes from the subtree's own drawer (pearl--buffer-issue-group-label), so the regroup matches a server-grouped render without a refetch. It works from either starting state. It climbs to the level-1 view parent, captures every issue (level-2 in a flat buffer, level-3 under a group heading in a grouped one) normalized to a canonical level, then re-emits them flat at level 2 or sectioned under level-2 group headings at level 3. Group headings are regenerated, so a stale one never lingers. A non-issue level-2 subtree is kept and placed after the groups rather than dropped. pearl--shift-heading-levels is the level mechanic, isolated and tested both directions. 29 tests cover the shift, grouping by each dimension, the unset bucket, regrouping to a new dimension, flatten and round-trip, edit preservation, malformed-last, and no-issues.
Diffstat (limited to 'tests')
-rw-r--r--tests/test-pearl-grouping.el144
1 files changed, 144 insertions, 0 deletions
diff --git a/tests/test-pearl-grouping.el b/tests/test-pearl-grouping.el
index 10902f1..af9c969 100644
--- a/tests/test-pearl-grouping.el
+++ b/tests/test-pearl-grouping.el
@@ -142,5 +142,149 @@
(should (equal (plist-get (pearl--read-active-source) :client-group)
"workflowState"))))
+;;; in-place regroup core (phase 2)
+
+(defconst test-pearl-grouping--flat
+ (concat
+ "#+LINEAR-SOURCE: (:type filter :name \"F\" :filter (:open t))\n\n"
+ "* F\n"
+ "** TODO SE-1: Alpha\n:PROPERTIES:\n:LINEAR-ID: i1\n"
+ ":LINEAR-STATE-NAME: In Progress\n:LINEAR-PROJECT-NAME: Apollo\n"
+ ":LINEAR-ASSIGNEE-NAME: Eric\n:LINEAR-PRIORITY: 2\n:END:\n"
+ "Body alpha.\n"
+ "*** Comments\n"
+ "**** Comment by Someone\n:PROPERTIES:\n:LINEAR-COMMENT-ID: c1\n:END:\nA comment.\n"
+ "** TODO SE-2: Beta\n:PROPERTIES:\n:LINEAR-ID: i2\n"
+ ":LINEAR-STATE-NAME: Todo\n:LINEAR-PROJECT-NAME: Apollo\n"
+ ":LINEAR-PRIORITY: 1\n:END:\n"
+ "Body beta.\n")
+ "A flat two-issue buffer with drawer fields for status/project/assignee/priority.
+SE-1 has a comment subtree; SE-2 has no assignee.")
+
+(defun test-pearl-grouping--issue-level (id)
+ "Return the heading level of the issue with LINEAR-ID = ID, or nil."
+ (save-excursion
+ (goto-char (point-min))
+ (catch 'found
+ (while (re-search-forward "^\\*+ " nil t)
+ (when (equal (org-entry-get nil "LINEAR-ID") id)
+ (throw 'found (org-current-level))))
+ nil)))
+
+(defun test-pearl-grouping--group-headings ()
+ "Return the level-2 non-issue headings (group labels) in document order."
+ (let (out)
+ (save-excursion
+ (goto-char (point-min))
+ (while (re-search-forward "^\\*\\* \\(.*\\)$" nil t)
+ ;; Capture the heading text before `org-entry-get', whose internal
+ ;; regex clobbers the match data.
+ (let ((text (match-string-no-properties 1)))
+ (unless (org-entry-get nil "LINEAR-ID")
+ (push text out)))))
+ (nreverse out)))
+
+(ert-deftest test-pearl-shift-heading-levels-demote ()
+ "A +1 shift adds a star to every heading line, leaving body lines alone."
+ (should (equal (pearl--shift-heading-levels "** H\n:PROPERTIES:\n:X: 1\n:END:\nbody\n*** C\n" 1)
+ "*** H\n:PROPERTIES:\n:X: 1\n:END:\nbody\n**** C\n")))
+
+(ert-deftest test-pearl-shift-heading-levels-promote ()
+ "A -1 shift removes a star from every heading line."
+ (should (equal (pearl--shift-heading-levels "*** H\nbody\n**** C\n" -1)
+ "** H\nbody\n*** C\n")))
+
+(ert-deftest test-pearl-shift-heading-levels-zero-is-identity ()
+ "A zero delta returns the text unchanged."
+ (should (equal (pearl--shift-heading-levels "** H\nbody\n" 0) "** H\nbody\n")))
+
+(ert-deftest test-pearl-regroup-flat-by-status ()
+ "Grouping a flat buffer by status sections issues under level-2 group headings."
+ (test-pearl-grouping--in-org test-pearl-grouping--flat
+ (should (eq (pearl--regroup-issue-subtrees "workflowState") t))
+ (should (equal (test-pearl-grouping--group-headings) '("In Progress" "Todo")))
+ (should (= (test-pearl-grouping--issue-level "i1") 3))
+ (should (= (test-pearl-grouping--issue-level "i2") 3))))
+
+(ert-deftest test-pearl-regroup-flat-by-project-single-group ()
+ "Grouping by a shared dimension puts both issues under one group heading."
+ (test-pearl-grouping--in-org test-pearl-grouping--flat
+ (should (eq (pearl--regroup-issue-subtrees "project") t))
+ (should (equal (test-pearl-grouping--group-headings) '("Apollo")))
+ (should (= (test-pearl-grouping--issue-level "i1") 3))
+ (should (= (test-pearl-grouping--issue-level "i2") 3))))
+
+(ert-deftest test-pearl-regroup-unset-dimension-bucket ()
+ "An issue with no value for the dimension lands in the \"No X\" bucket."
+ (test-pearl-grouping--in-org test-pearl-grouping--flat
+ (should (eq (pearl--regroup-issue-subtrees "assignee") t))
+ (should (member "No assignee" (test-pearl-grouping--group-headings)))))
+
+(ert-deftest test-pearl-regroup-priority-bucket-label ()
+ "Priority grouping uses the Linear priority names (High, Urgent)."
+ (test-pearl-grouping--in-org test-pearl-grouping--flat
+ (should (eq (pearl--regroup-issue-subtrees "priority") t))
+ (should (equal (sort (copy-sequence (test-pearl-grouping--group-headings)) #'string<)
+ '("High" "Urgent")))))
+
+(ert-deftest test-pearl-regroup-none-flattens ()
+ "Ungrouping a grouped buffer returns issues to level 2 with no group headings."
+ (test-pearl-grouping--in-org test-pearl-grouping--flat
+ (pearl--regroup-issue-subtrees "workflowState")
+ (should (eq (pearl--regroup-issue-subtrees nil) t))
+ (should (null (test-pearl-grouping--group-headings)))
+ (should (= (test-pearl-grouping--issue-level "i1") 2))
+ (should (= (test-pearl-grouping--issue-level "i2") 2))))
+
+(ert-deftest test-pearl-regroup-grouped-to-different-dimension ()
+ "Regrouping an already-grouped buffer by a new dimension re-buckets the issues."
+ (test-pearl-grouping--in-org test-pearl-grouping--flat
+ (pearl--regroup-issue-subtrees "workflowState")
+ (should (eq (pearl--regroup-issue-subtrees "project") t))
+ (should (equal (test-pearl-grouping--group-headings) '("Apollo")))
+ (should (= (test-pearl-grouping--issue-level "i1") 3))
+ (should (= (test-pearl-grouping--issue-level "i2") 3))))
+
+(ert-deftest test-pearl-regroup-roundtrips-flat ()
+ "Group then ungroup restores the flat shape and keeps both issues."
+ (test-pearl-grouping--in-org test-pearl-grouping--flat
+ (pearl--regroup-issue-subtrees "workflowState")
+ (pearl--regroup-issue-subtrees nil)
+ (should (= (test-pearl-grouping--issue-level "i1") 2))
+ (should (= (test-pearl-grouping--issue-level "i2") 2))
+ (should (equal (sort (mapcar #'car (pearl--issue-subtree-markers)) #'string<)
+ '("i1" "i2")))))
+
+(ert-deftest test-pearl-regroup-preserves-local-edits ()
+ "Regroup moves subtrees byte-for-byte: an unsaved body edit survives."
+ (test-pearl-grouping--in-org test-pearl-grouping--flat
+ (goto-char (point-min))
+ (search-forward "Body alpha.")
+ (replace-match "Body alpha EDITED LOCALLY.")
+ (should (eq (pearl--regroup-issue-subtrees "workflowState") t))
+ (goto-char (point-min))
+ (should (search-forward "Body alpha EDITED LOCALLY." nil t))
+ ;; the comment child rode along under the regrouped issue
+ (should (= (test-pearl-grouping--issue-level "i1") 3))
+ (goto-char (point-min))
+ (should (search-forward "A comment." nil t))))
+
+(ert-deftest test-pearl-regroup-keeps-malformed-subtree-last ()
+ "A non-issue level-2 subtree is kept, placed after the issue groups."
+ (test-pearl-grouping--in-org
+ (concat test-pearl-grouping--flat
+ "** Stray non-issue heading\nSome notes.\n")
+ (should (eq (pearl--regroup-issue-subtrees "workflowState") t))
+ (let ((headings (test-pearl-grouping--group-headings)))
+ (should (member "Stray non-issue heading" headings))
+ ;; the stray heading sorts after the real group headings
+ (should (equal (car (last headings)) "Stray non-issue heading")))))
+
+(ert-deftest test-pearl-regroup-no-issues ()
+ "An issue-free buffer reports `no-issues' and is left alone."
+ (test-pearl-grouping--in-org
+ "#+LINEAR-SOURCE: (:type filter :name \"F\" :filter nil)\n\n* F\n"
+ (should (eq (pearl--regroup-issue-subtrees "workflowState") 'no-issues))))
+
(provide 'test-pearl-grouping)
;;; test-pearl-grouping.el ends here