aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-pearl-merge.el79
-rw-r--r--tests/test-pearl-output.el47
-rw-r--r--tests/test-pearl-refresh.el4
-rw-r--r--tests/test-pearl-views.el70
4 files changed, 198 insertions, 2 deletions
diff --git a/tests/test-pearl-merge.el b/tests/test-pearl-merge.el
index 931849d..9f9307c 100644
--- a/tests/test-pearl-merge.el
+++ b/tests/test-pearl-merge.el
@@ -72,6 +72,22 @@
"#+LINEAR-TRUNCATED: no\n\n"
(mapconcat #'pearl--format-issue-as-org-entry issues "")))
+(defun test-pearl-merge--issue-st (id title state-name state-type)
+ "A normalized issue plist in state STATE-NAME/STATE-TYPE, for grouping tests."
+ (list :id id :identifier (concat "ENG-" id) :title title
+ :description (concat "Desc " id ".")
+ :priority 2 :url (concat "https://linear.app/x/" id)
+ :updated-at "2026-05-23T03:00:00.000Z"
+ :state (list :id (concat "s-" state-name) :name state-name :type state-type)
+ :team (list :id "t1" :key "ENG" :name "Engineering")))
+
+(defun test-pearl-merge--grouped-buffer (grouping &rest issues)
+ "Render ISSUES into a grouped view buffer (no help header, network stubbed)."
+ (let ((pearl-help-header nil))
+ (cl-letf (((symbol-function 'pearl--team-states) (lambda (_team-id) nil)))
+ (pearl--build-org-content
+ issues (list :type 'view :name "By status" :id "v1" :group grouping)))))
+
;;; --merge-issues-into-buffer
(ert-deftest test-pearl-merge-updates-existing-in-place ()
@@ -123,6 +139,69 @@
(goto-char (point-min))
(should-not (re-search-forward "Beta" nil t)))))
+;;; grouped-view merge (issueGrouping sections)
+
+(ert-deftest test-pearl-merge-grouped-update-preserves-level ()
+ "Updating an issue in a grouped buffer re-renders it at level 3 (under its
+group), not promoted to level 2 the way a flat append would render it."
+ (test-pearl-merge--in-org
+ (test-pearl-merge--grouped-buffer
+ "workflowState"
+ (test-pearl-merge--issue-st "a" "Alpha" "Todo" "unstarted")
+ (test-pearl-merge--issue-st "b" "Beta" "Done" "completed"))
+ (let ((counts (pearl--merge-issues-into-buffer
+ (list (test-pearl-merge--issue-st "a" "Alpha Renamed" "Todo" "unstarted")
+ (test-pearl-merge--issue-st "b" "Beta" "Done" "completed"))
+ "workflowState")))
+ (should (= 2 (plist-get counts :updated)))
+ (goto-char (point-min))
+ (should (re-search-forward "^\\*\\*\\* .*Alpha Renamed" nil t))
+ ;; Not promoted to a level-2 heading.
+ (should-not (save-excursion (goto-char (point-min))
+ (re-search-forward "^\\*\\* [A-Z].*Alpha Renamed" nil t))))))
+
+(ert-deftest test-pearl-merge-grouped-new-issue-joins-existing-group ()
+ "A new issue whose group already exists is inserted under that group's section
+at level 3, not appended at the buffer end."
+ (test-pearl-merge--in-org
+ (test-pearl-merge--grouped-buffer
+ "workflowState"
+ (test-pearl-merge--issue-st "a" "Alpha" "Todo" "unstarted")
+ (test-pearl-merge--issue-st "b" "Beta" "Done" "completed"))
+ (let ((counts (pearl--merge-issues-into-buffer
+ (list (test-pearl-merge--issue-st "a" "Alpha" "Todo" "unstarted")
+ (test-pearl-merge--issue-st "b" "Beta" "Done" "completed")
+ (test-pearl-merge--issue-st "c" "Gamma" "Todo" "unstarted"))
+ "workflowState")))
+ (should (= 1 (plist-get counts :added)))
+ ;; Gamma renders at level 3, inside the Todo section (before Done).
+ (goto-char (point-min))
+ (let ((todo (re-search-forward "^\\*\\* Todo$" nil t))
+ (gamma (re-search-forward "^\\*\\*\\* .*Gamma" nil t))
+ (done (save-excursion (goto-char (point-min))
+ (re-search-forward "^\\*\\* Done$" nil t))))
+ (should (and todo gamma done))
+ (should (< todo gamma))
+ (should (< gamma done))))))
+
+(ert-deftest test-pearl-merge-grouped-new-issue-creates-missing-group ()
+ "A new issue whose group doesn't exist yet gets a fresh `** LABEL' section
+created for it, with the issue at level 3 under it."
+ (test-pearl-merge--in-org
+ (test-pearl-merge--grouped-buffer
+ "workflowState"
+ (test-pearl-merge--issue-st "a" "Alpha" "Todo" "unstarted"))
+ (should-not (save-excursion (re-search-forward "^\\*\\* In Progress$" nil t)))
+ (let ((counts (pearl--merge-issues-into-buffer
+ (list (test-pearl-merge--issue-st "a" "Alpha" "Todo" "unstarted")
+ (test-pearl-merge--issue-st "d" "Delta" "In Progress" "started"))
+ "workflowState")))
+ (should (= 1 (plist-get counts :added)))
+ (goto-char (point-min))
+ (let ((grp (re-search-forward "^\\*\\* In Progress$" nil t)))
+ (should grp)
+ (should (re-search-forward "^\\*\\*\\* .*Delta" nil t))))))
+
(ert-deftest test-pearl-merge-keeps-unpushed-edit-on-update ()
"An existing subtree with unpushed body edits is kept, not overwritten."
(test-pearl-merge--in-org
diff --git a/tests/test-pearl-output.el b/tests/test-pearl-output.el
index 8b02fcb..f02df5f 100644
--- a/tests/test-pearl-output.el
+++ b/tests/test-pearl-output.el
@@ -70,6 +70,51 @@
(should (string-match-p "^#\\+title:" out))
(should-not (string-match-p "^\\*\\*\\* " out))))
+;;; --build-org-content grouping (issueGrouping -> Org sections)
+
+(ert-deftest test-pearl-build-org-content-flat-startup-show2levels ()
+ "An ungrouped view folds to show2levels and renders issues at level 2."
+ (let* ((issues '((:id "1" :identifier "SE-1" :title "One"
+ :state (:name "Started" :type "started") :priority 0)))
+ (source '(:type view :name "Flat" :id "v1"))
+ (out (pearl--build-org-content issues source)))
+ (should (string-match-p "^#\\+STARTUP: show2levels$" out))
+ (should (string-match-p "^\\*\\* STARTED " out))
+ (should-not (string-match-p "^\\*\\*\\* " out))))
+
+(ert-deftest test-pearl-build-org-content-grouped-sections-and-startup ()
+ "A grouped view folds to show3levels, emits a level-2 group heading per group
+in first-appearance order, and renders issues at level 3 under them."
+ (let* ((issues '((:id "1" :identifier "SE-1" :title "One"
+ :state (:name "Started" :type "started") :priority 0)
+ (:id "2" :identifier "SE-2" :title "Two"
+ :state (:name "Backlog" :type "backlog") :priority 0)
+ (:id "3" :identifier "SE-3" :title "Three"
+ :state (:name "Started" :type "started") :priority 0)))
+ (source '(:type view :name "By status" :id "v1" :group "workflowState"))
+ (out (pearl--build-org-content issues source)))
+ (should (string-match-p "^#\\+STARTUP: show3levels$" out))
+ (should (string-match-p "^\\*\\* Started$" out))
+ (should (string-match-p "^\\*\\* Backlog$" out))
+ ;; Issues live at level 3 under the group headings.
+ (should (string-match-p "^\\*\\*\\* STARTED " out))
+ ;; First-appearance order: SE-1 (Started) before SE-2 (Backlog).
+ (should (< (string-match "^\\*\\* Started$" out)
+ (string-match "^\\*\\* Backlog$" out)))
+ ;; The group heading carries no LINEAR-ID, so save/merge skip it.
+ (let ((started (string-match "^\\*\\* Started$" out))
+ (next-id (string-match "LINEAR-ID:" out)))
+ (should started)
+ (should (< started next-id)))))
+
+(ert-deftest test-pearl-build-org-content-grouped-source-roundtrips ()
+ "A grouped view's source (with :group) reads back from the rendered header."
+ (let* ((source '(:type view :name "By project" :id "v1" :group "project"))
+ (out (pearl--build-org-content '() source)))
+ (with-temp-buffer
+ (insert out)
+ (should (equal source (pearl--read-active-source))))))
+
(ert-deftest test-pearl-build-org-content-title-uses-view-name-verbatim ()
"The view name in the file title matches Linear's capitalization verbatim,
independent of `pearl-title-case-headings' (which governs issue headings only)."
@@ -336,7 +381,7 @@ refresh prompt stubbed to return CHOICE. BUF and TMP are bound in BODY."
(let ((merged nil))
(test-pearl-output--with-dirty-buffer ?m
(cl-letf (((symbol-function 'pearl--merge-issues-into-buffer)
- (lambda (_issues) (setq merged t) '(:updated 1 :added 0 :dropped 0 :skipped 0)))
+ (lambda (_issues &optional _grouping) (setq merged t) '(:updated 1 :added 0 :dropped 0 :skipped 0)))
((symbol-function 'pearl--update-source-header) #'ignore)
((symbol-function 'pearl--update-derived-todo-header) #'ignore)
((symbol-function 'pearl-highlight-comments) #'ignore)
diff --git a/tests/test-pearl-refresh.el b/tests/test-pearl-refresh.el
index d09c96c..199b346 100644
--- a/tests/test-pearl-refresh.el
+++ b/tests/test-pearl-refresh.el
@@ -93,7 +93,9 @@
(re-search-forward "Stale Title")
(pearl-refresh-current-issue)
(goto-char (point-min))
- (re-search-forward "^\\*\\* ")
+ ;; refresh preserves the issue's heading level (here level 3) rather than
+ ;; flattening it -- a grouped view's issues stay at level 3 on refresh
+ (re-search-forward "^\\*\\*\\* ")
;; heading + drawer reflect the remote
(should (string-match-p "Refreshed Title" (thing-at-point 'line t)))
(should (string= "In Progress" (org-entry-get nil "LINEAR-STATE-NAME")))
diff --git a/tests/test-pearl-views.el b/tests/test-pearl-views.el
index 3675200..7c8ec80 100644
--- a/tests/test-pearl-views.el
+++ b/tests/test-pearl-views.el
@@ -325,6 +325,76 @@ the pick-source path showed a view's completed/canceled issues and ignored sort.
;; The favorite's own keys survive the enrichment.
(should (string= "https://x" (plist-get rendered-source :url))))))
+;;; --view-grouping-active-p / --view-group-label / --group-issues
+
+(ert-deftest test-pearl-view-grouping-active-p-supported-types ()
+ "The single-valued grouping dimensions are active in v1."
+ (dolist (g '("workflowState" "project" "assignee" "priority" "cycle"))
+ (should (pearl--view-grouping-active-p g))))
+
+(ert-deftest test-pearl-view-grouping-active-p-deferred-and-none ()
+ "Label-grouping (multi-valued, deferred), none, nil, and unknown values are
+inactive, so the view renders flat."
+ (dolist (g '("label" "none" nil "someFutureGrouping"))
+ (should-not (pearl--view-grouping-active-p g))))
+
+(ert-deftest test-pearl-view-group-label-by-dimension ()
+ "Each supported grouping resolves an issue to its group label."
+ (let ((issue '(:state (:name "In Progress" :type "started")
+ :project (:name "Orchestration")
+ :assignee (:name "Eric")
+ :priority 1
+ :cycle (:number 4 :name "Sprint 4"))))
+ (should (string= "In Progress" (pearl--view-group-label issue "workflowState")))
+ (should (string= "Orchestration" (pearl--view-group-label issue "project")))
+ (should (string= "Eric" (pearl--view-group-label issue "assignee")))
+ (should (string= "Urgent" (pearl--view-group-label issue "priority")))
+ (should (string= "Sprint 4" (pearl--view-group-label issue "cycle")))))
+
+(ert-deftest test-pearl-view-group-label-missing-values ()
+ "A missing dimension value gets a stable \"No X\" bucket label, matching how
+Linear shows unset issues in a grouped view."
+ (let ((issue '(:priority 0)))
+ (should (string= "No status" (pearl--view-group-label issue "workflowState")))
+ (should (string= "No project" (pearl--view-group-label issue "project")))
+ (should (string= "No assignee" (pearl--view-group-label issue "assignee")))
+ (should (string= "No cycle" (pearl--view-group-label issue "cycle")))
+ (should (string= "None" (pearl--view-group-label issue "priority")))))
+
+(ert-deftest test-pearl-view-group-label-cycle-number-fallback ()
+ "A cycle with no name falls back to its number."
+ (should (string= "Cycle 7"
+ (pearl--view-group-label '(:cycle (:number 7)) "cycle"))))
+
+(ert-deftest test-pearl-group-issues-inactive-is-nil ()
+ "An inactive grouping returns nil, signaling the caller to render flat."
+ (should-not (pearl--group-issues '((:id "a" :state (:name "Todo"))) "none"))
+ (should-not (pearl--group-issues '((:id "a" :state (:name "Todo"))) nil)))
+
+(ert-deftest test-pearl-group-issues-orders-by-first-appearance ()
+ "Groups appear in the order their first issue does; issues keep their input
+order within a group (the layer-1 sort is preserved inside each section)."
+ (let* ((issues '((:id "1" :state (:name "Started"))
+ (:id "2" :state (:name "Backlog"))
+ (:id "3" :state (:name "Started"))
+ (:id "4" :state (:name "Backlog"))))
+ (groups (pearl--group-issues issues "workflowState")))
+ (should (equal '("Started" "Backlog") (mapcar #'car groups)))
+ (should (equal '("1" "3") (mapcar (lambda (i) (plist-get i :id))
+ (cdr (assoc "Started" groups)))))
+ (should (equal '("2" "4") (mapcar (lambda (i) (plist-get i :id))
+ (cdr (assoc "Backlog" groups)))))))
+
+;;; --view-node-prefs carries grouping
+
+(ert-deftest test-pearl-view-node-prefs-extracts-grouping ()
+ "The view node's issueGrouping is read into :group alongside sort and
+show-completed, so run/refresh can section the issues."
+ (let ((prefs (pearl--view-node-prefs
+ '((id . "v1")
+ (viewPreferencesValues (issueGrouping . "workflowState"))))))
+ (should (string= "workflowState" (plist-get prefs :group)))))
+
;;; refresh-current-view, view branch
(ert-deftest test-pearl-refresh-current-view-runs-view-source ()