aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-03 07:52:30 -0500
committerCraig Jennings <c@cjennings.net>2026-06-03 07:52:30 -0500
commit580309bb65a5e529c7aab5e58d3983a37c471023 (patch)
tree10ebe78cbd8909a39ba126c77a560bc96174817a /tests
parent6ef196e306320804b80a502dab29dbafd12285bc (diff)
downloadpearl-580309bb65a5e529c7aab5e58d3983a37c471023.tar.gz
pearl-580309bb65a5e529c7aab5e58d3983a37c471023.zip
fix(views): honor a Linear view's completed-issues display preference
A Linear view that hides completed issues still showed done and cancelled issues when run in pearl. The open-only restriction isn't in the view's filterData. It's a separate display preference, viewPreferencesValues.showCompletedIssues, which pearl fetched for sort but never applied. So the customView.issues connection returned every state type, and a "by status" or "open issues" view didn't look like itself. I now read showCompletedIssues alongside the sort fields and translate it to an IssueFilter passed to the connection, AND-combined with the view's own filter. "all" adds nothing, "none" excludes the closed state types (completed, canceled, duplicate), and a recency window (day/week/month/quarter/year) keeps open issues plus those completed or canceled within the window. I stamp the raw preference on the source so a refresh rebuilds the window relative to the refresh time rather than first-run time. Both entry points route through a shared pearl--view-node-prefs resolver, so pearl-run-linear-view and a favorited view picked in pearl-pick-source behave the same. That also closes a latent gap where the pick-source path ignored the view's configured sort. Verified live against the workspace: a "none" view returns only open issues, and a "week" view returns open issues plus those closed within seven days, with none older leaking through.
Diffstat (limited to 'tests')
-rw-r--r--tests/test-pearl-views.el161
1 files changed, 157 insertions, 4 deletions
diff --git a/tests/test-pearl-views.el b/tests/test-pearl-views.el
index 7a211df..3675200 100644
--- a/tests/test-pearl-views.el
+++ b/tests/test-pearl-views.el
@@ -131,6 +131,75 @@ issues connection, so a view runs under a chosen order."
(should (string-match-p "\\$sort: \\[IssueSortInput!\\]" q))
(should (string-match-p "sort: \\$sort" q))))
+;;; --view-completed-filter (showCompletedIssues -> IssueFilter)
+
+(ert-deftest test-pearl-view-completed-filter-all-is-nil ()
+ "showCompletedIssues \"all\" means show everything, so no extra filter is
+added — today's behavior, and the open clause never hides anything."
+ (should-not (pearl--view-completed-filter "all")))
+
+(ert-deftest test-pearl-view-completed-filter-nil-is-nil ()
+ "A view with no recorded preference adds no filter."
+ (should-not (pearl--view-completed-filter nil)))
+
+(ert-deftest test-pearl-view-completed-filter-unknown-is-nil ()
+ "An unrecognized preference value adds no filter rather than risk hiding open
+work — pearl degrades to showing everything, the same as \"all\"."
+ (should-not (pearl--view-completed-filter "someFutureValue")))
+
+(ert-deftest test-pearl-view-completed-filter-none-excludes-closed-types ()
+ "showCompletedIssues \"none\" restricts to open issues by excluding the closed
+state types (completed, canceled, duplicate) that Linear's UI hides."
+ (should (equal '(("state" ("type" ("nin" . ["completed" "canceled" "duplicate"]))))
+ (pearl--view-completed-filter "none"))))
+
+(ert-deftest test-pearl-view-completed-filter-window-ors-open-and-recent ()
+ "A recency window (e.g. \"week\") shows open issues OR issues completed or
+canceled within the window, mirroring Linear's \"completed in the past week\"."
+ (let* ((now (date-to-time "2026-06-08T12:00:00Z"))
+ (f (pearl--view-completed-filter "week" now)))
+ (should (equal
+ '(("or" . [(("state" ("type" ("nin" . ["completed" "canceled" "duplicate"]))))
+ (("completedAt" ("gte" . "2026-06-01T12:00:00.000Z")))
+ (("canceledAt" ("gte" . "2026-06-01T12:00:00.000Z")))]))
+ f))))
+
+(ert-deftest test-pearl-view-issues-query-accepts-filter ()
+ "The view issues query declares the filter variable and forwards it to the
+issues connection, so a view can drop completed issues server-side."
+ (let ((q (pearl--view-issues-query)))
+ (should (string-match-p "\\$filter: IssueFilter" q))
+ (should (string-match-p "filter: \\$filter" q))))
+
+(ert-deftest test-pearl-query-view-async-forwards-filter ()
+ "A non-nil filter is sent as the `filter' GraphQL variable."
+ (let ((sent 'unset))
+ (cl-letf (((symbol-function 'pearl--graphql-request-async)
+ (lambda (_q vars success &optional _err)
+ (setq sent (cdr (assoc "filter" vars)))
+ (funcall success
+ '((data (customView
+ (issues (nodes . [])
+ (pageInfo (hasNextPage . :json-false)
+ (endCursor))))))))))
+ (pearl--query-view-async "v1" #'ignore nil '(("state" "x")))
+ (should (equal '(("state" "x")) sent)))))
+
+(ert-deftest test-pearl-query-view-async-omits-nil-filter ()
+ "With no filter, no `filter' variable is sent (the connection sees no filter,
+matching a view whose preference shows all issues)."
+ (let ((had-key 'unset))
+ (cl-letf (((symbol-function 'pearl--graphql-request-async)
+ (lambda (_q vars success &optional _err)
+ (setq had-key (and (assoc "filter" vars) t))
+ (funcall success
+ '((data (customView
+ (issues (nodes . [])
+ (pageInfo (hasNextPage . :json-false)
+ (endCursor))))))))))
+ (pearl--query-view-async "v1" #'ignore nil nil)
+ (should-not had-key))))
+
(ert-deftest test-pearl-query-view-async-defaults-sort-to-updated-desc ()
"With no explicit sort, a view runs under updatedAt-desc rather than Linear's
createdAt-desc API default, so its order matches the rest of pearl."
@@ -158,7 +227,7 @@ createdAt-desc API default, so its order matches the rest of pearl."
'(((id . "v1") (name . "My View")))))
((symbol-function 'pearl--org-url-key) (lambda (&optional _) "deepsat"))
((symbol-function 'pearl--query-view-async)
- (lambda (id cb &optional _sort) (setq ran-id id)
+ (lambda (id cb &optional _sort _filter) (setq ran-id id)
(funcall cb (pearl--make-query-result 'ok :issues nil))))
((symbol-function 'pearl--render-query-result)
(lambda (_result source) (setq rendered-source source))))
@@ -180,7 +249,7 @@ records it on the source so a later refresh reuses the same order."
(viewOrderingDirection . "desc"))))))
((symbol-function 'pearl--org-url-key) (lambda (&optional _) "deepsat"))
((symbol-function 'pearl--query-view-async)
- (lambda (_id cb &optional sort) (setq passed-sort sort)
+ (lambda (_id cb &optional sort _filter) (setq passed-sort sort)
(funcall cb (pearl--make-query-result 'ok :issues nil))))
((symbol-function 'pearl--render-query-result)
(lambda (_result source) (setq rendered-source source))))
@@ -189,6 +258,73 @@ records it on the source so a later refresh reuses the same order."
(should (equal (vector '(("priority" ("order" . "Descending"))))
(plist-get rendered-source :sort))))))
+(ert-deftest test-pearl-run-view-applies-and-stamps-show-completed ()
+ "Running a view reads its showCompletedIssues preference, passes the matching
+filter to the query, and records the raw preference on the source so a later
+refresh rebuilds the window relative to the refresh time."
+ (let ((passed-filter 'unset) (rendered-source nil))
+ (cl-letf (((symbol-function 'pearl--custom-views)
+ (lambda (&optional _force)
+ '(((id . "v1") (name . "My View")
+ (viewPreferencesValues (showCompletedIssues . "none"))))))
+ ((symbol-function 'pearl--org-url-key) (lambda (&optional _) "deepsat"))
+ ((symbol-function 'pearl--query-view-async)
+ (lambda (_id cb &optional _sort filter) (setq passed-filter filter)
+ (funcall cb (pearl--make-query-result 'ok :issues nil))))
+ ((symbol-function 'pearl--render-query-result)
+ (lambda (_result source) (setq rendered-source source))))
+ (pearl-run-linear-view "My View")
+ (should (equal '(("state" ("type" ("nin" . ["completed" "canceled" "duplicate"]))))
+ passed-filter))
+ (should (string= "none" (plist-get rendered-source :show-completed))))))
+
+;;; --view-node-prefs and --run-view-source (favorited view dispatch)
+
+(ert-deftest test-pearl-view-node-prefs-extracts-sort-and-show-completed ()
+ "A Custom View node's preferences resolve to its IssueSortInput sort and raw
+showCompletedIssues string."
+ (let ((prefs (pearl--view-node-prefs
+ '((id . "v1")
+ (viewPreferencesValues (viewOrdering . "priority")
+ (viewOrderingDirection . "desc")
+ (showCompletedIssues . "none"))))))
+ (should (equal (vector '(("priority" ("order" . "Descending"))))
+ (plist-get prefs :sort)))
+ (should (string= "none" (plist-get prefs :show-completed)))))
+
+(ert-deftest test-pearl-view-node-prefs-nil-node-is-all-nil ()
+ "A nil node (a favorite referencing a view the list query didn't return) yields
+nil sort and nil show-completed, so the caller falls back to house defaults."
+ (let ((prefs (pearl--view-node-prefs nil)))
+ (should-not (plist-get prefs :sort))
+ (should-not (plist-get prefs :show-completed))))
+
+(ert-deftest test-pearl-run-view-source-resolves-prefs-by-id-and-stamps ()
+ "A favorited view source (no preferences of its own) resolves the view's sort
+and showCompletedIssues from the cached Custom Views, forwards both to the query,
+and stamps them on the source so a refresh reuses them — closing the gap where
+the pick-source path showed a view's completed/canceled issues and ignored sort."
+ (let ((passed-sort 'unset) (passed-filter 'unset) (rendered-source nil))
+ (cl-letf (((symbol-function 'pearl--custom-views)
+ (lambda (&optional _force)
+ '(((id . "v1") (name . "My View")
+ (viewPreferencesValues (viewOrdering . "priority")
+ (viewOrderingDirection . "desc")
+ (showCompletedIssues . "none"))))))
+ ((symbol-function 'pearl--query-view-async)
+ (lambda (_id cb &optional sort filter)
+ (setq passed-sort sort passed-filter filter)
+ (funcall cb (pearl--make-query-result 'ok :issues nil))))
+ ((symbol-function 'pearl--render-query-result)
+ (lambda (_result source) (setq rendered-source source))))
+ (pearl--run-view-source '(:type view :name "My View" :id "v1" :url "https://x"))
+ (should (equal (vector '(("priority" ("order" . "Descending")))) passed-sort))
+ (should (equal '(("state" ("type" ("nin" . ["completed" "canceled" "duplicate"]))))
+ passed-filter))
+ (should (string= "none" (plist-get rendered-source :show-completed)))
+ ;; The favorite's own keys survive the enrichment.
+ (should (string= "https://x" (plist-get rendered-source :url))))))
+
;;; refresh-current-view, view branch
(ert-deftest test-pearl-refresh-current-view-runs-view-source ()
@@ -200,7 +336,7 @@ records it on the source so a later refresh reuses the same order."
(prin1-to-string source)))
(org-mode)
(cl-letf (((symbol-function 'pearl--query-view-async)
- (lambda (id cb &optional _sort) (setq view-ran id)
+ (lambda (id cb &optional _sort _filter) (setq view-ran id)
(funcall cb (pearl--make-query-result 'ok :issues nil))))
((symbol-function 'pearl--merge-query-result)
(lambda (&rest _) nil)))
@@ -217,13 +353,30 @@ list keeps the order the view was run under."
(insert (format "#+LINEAR-SOURCE: %s\n\n" (prin1-to-string source)))
(org-mode)
(cl-letf (((symbol-function 'pearl--query-view-async)
- (lambda (_id cb &optional sort) (setq passed-sort sort)
+ (lambda (_id cb &optional sort _filter) (setq passed-sort sort)
(funcall cb (pearl--make-query-result 'ok :issues nil))))
((symbol-function 'pearl--merge-query-result)
(lambda (&rest _) nil)))
(pearl-refresh-current-view)
(should (equal [(("priority" ("order" . "Descending")))] passed-sort))))))
+(ert-deftest test-pearl-refresh-current-view-rebuilds-show-completed-filter ()
+ "Refreshing a view source rebuilds the completed-issues filter from the raw
+`:show-completed' it recorded, so an open-only view stays open-only on refresh."
+ (let ((passed-filter 'unset)
+ (source '(:type view :name "My View" :id "v1" :show-completed "none")))
+ (with-temp-buffer
+ (insert (format "#+LINEAR-SOURCE: %s\n\n" (prin1-to-string source)))
+ (org-mode)
+ (cl-letf (((symbol-function 'pearl--query-view-async)
+ (lambda (_id cb &optional _sort filter) (setq passed-filter filter)
+ (funcall cb (pearl--make-query-result 'ok :issues nil))))
+ ((symbol-function 'pearl--merge-query-result)
+ (lambda (&rest _) nil)))
+ (pearl-refresh-current-view)
+ (should (equal '(("state" ("type" ("nin" . ["completed" "canceled" "duplicate"]))))
+ passed-filter))))))
+
;;; open-current-view-in-linear
(ert-deftest test-pearl-open-current-view-visits-url ()