diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-06 15:11:15 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-06 15:11:15 -0500 |
| commit | 690faf7538bb7fcd47367b4801923beea40a0ff1 (patch) | |
| tree | ffc2f2b6deb1be447582981f3935e9941d732ad4 /tests | |
| parent | 0a3794ccfc56045a2e4155538c145021447d09e3 (diff) | |
| download | pearl-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.el | 144 |
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 |
