aboutsummaryrefslogtreecommitdiff
path: root/tests/test-org-drill-statistics-render-attention.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-31 08:35:16 -0500
committerCraig Jennings <c@cjennings.net>2026-05-31 08:35:16 -0500
commit26cc4472dea261a1ad13fbee8fb6a91b019f77bb (patch)
treea7063337b05c3ea278a5b910d0f1420de033dfe8 /tests/test-org-drill-statistics-render-attention.el
parent532ce532465834ce06238648ba1490c48bed29ca (diff)
downloadorg-drill-26cc4472dea261a1ad13fbee8fb6a91b019f77bb.tar.gz
org-drill-26cc4472dea261a1ad13fbee8fb6a91b019f77bb.zip
feat: add the org-drill statistics dashboard renderer
Step 1 shipped the session-log data layer. This is the renderer on top of it. org-drill-statistics opens a read-only dashboard with five sections: an overview (card counts plus a last-session recap), trends (reviews-per-day and pass-rate-per-day quadrant-block sparklines over the trend window, plus a 12-week table), a quality histogram, a needs-attention view (leech candidates, long-overdue, and forgotten-new cards), and a 7-day forecast counted from SCHEDULED dates. A buffer-wide filter (scope, range, algorithm) sits in the header and cycles with s/r/a. The other keys are q to bury, g to refresh, e for the CSV-export hook that lands next, and RET to follow the card link at point. The aggregation math lives in pure helpers (day-bucketing, sparkline scaling, weekly aggregates, the histogram, the attention selectors, forecast bucketing). The render helpers are thin string formatters over them, so the logic is unit-tested independently of the UI. New defcustoms tune the views: org-drill-statistics-trend-days, -forecast-days, -attention-row-limit, and -leech-quality-threshold. I added require 'calendar for the Monday week-start arithmetic in the weekly aggregates. CSV export and the manual and README entries are the step-3 follow-on.
Diffstat (limited to 'tests/test-org-drill-statistics-render-attention.el')
-rw-r--r--tests/test-org-drill-statistics-render-attention.el141
1 files changed, 141 insertions, 0 deletions
diff --git a/tests/test-org-drill-statistics-render-attention.el b/tests/test-org-drill-statistics-render-attention.el
new file mode 100644
index 0000000..b3f2375
--- /dev/null
+++ b/tests/test-org-drill-statistics-render-attention.el
@@ -0,0 +1,141 @@
+;;; test-org-drill-statistics-render-attention.el --- Tests for render-attention statistics -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; ERT tests for the org-drill statistics dashboard render-attention block.
+
+;;; Code:
+
+(require 'ert)
+(require 'org-drill)
+(require 'cl-lib)
+(require 'org)
+
+;;; tests/test-org-drill-statistics-render-attention.el -*- lexical-binding: t; -*-
+
+(defun test-org-drill-stats--attn-fixture ()
+ "Insert a fixture buffer of drill cards and return today's day number.
+Twelve leech candidates so the cap and footer are exercised, plus one
+long-overdue card and one forgotten-new card. Dates derive from the
+current time so the fixture never hardcodes today."
+ (let* ((today (org-drill-statistics--today-day))
+ (old-review (org-drill-time-to-inactive-org-timestamp
+ (org-time-string-to-time
+ (format-time-string
+ "%Y-%m-%d"
+ (time-subtract (current-time)
+ (days-to-time 400))))))
+ (old-added (format-time-string
+ "%Y-%m-%d"
+ (time-subtract (current-time) (days-to-time 30)))))
+ (insert "* Drill cards\n")
+ (dotimes (i 12)
+ (insert (format "** Leech %02d :drill:\n" i))
+ (insert ":PROPERTIES:\n")
+ (insert (format ":DRILL_FAILURE_COUNT: %d\n"
+ (+ org-drill-leech-failure-threshold 2)))
+ (insert (format ":DRILL_AVERAGE_QUALITY: %s\n"
+ (number-to-string (+ 1.0 (* 0.1 i)))))
+ (insert ":END:\n"))
+ (insert "** Overdue card :drill:\n")
+ (insert ":PROPERTIES:\n")
+ (insert (format ":DRILL_LAST_REVIEWED: %s\n" old-review))
+ (insert ":DRILL_FAILURE_COUNT: 0\n")
+ (insert ":END:\n")
+ (insert "** Forgotten card :drill:\n")
+ (insert ":PROPERTIES:\n")
+ (insert (format ":DATE_ADDED: %s\n" old-added))
+ (insert ":DRILL_TOTAL_REPEATS: 0\n")
+ (insert ":END:\n")
+ (org-mode)
+ today))
+
+(ert-deftest test-org-drill-statistics-attention-section-heading ()
+ "The rendered section opens with the Needs attention heading."
+ (with-temp-buffer
+ (test-org-drill-stats--attn-fixture)
+ (let ((out (org-drill-statistics--render-attention 'file)))
+ (should (string-match-p "^\\*\\* Needs attention$" out))
+ (should (string-match-p "^\\*\\*\\* Leech candidates$" out))
+ (should (string-match-p "^\\*\\*\\* Long overdue$" out))
+ (should (string-match-p "^\\*\\*\\* Forgotten new$" out)))))
+
+(ert-deftest test-org-drill-statistics-attention-leech-rows-as-links ()
+ "Leech candidates render as org links carrying card headings."
+ (with-temp-buffer
+ (test-org-drill-stats--attn-fixture)
+ (let ((out (org-drill-statistics--render-attention 'file)))
+ (should (string-match-p "| Card |" out))
+ (should (string-match-p "\\[\\[org-drill-card:[0-9]+\\]\\[Leech 00\\]\\]"
+ out)))))
+
+(ert-deftest test-org-drill-statistics-attention-cap-and-footer ()
+ "Twelve leeches over a 10 cap show 10 rows and a +2 more footer."
+ (with-temp-buffer
+ (test-org-drill-stats--attn-fixture)
+ (let* ((org-drill-statistics-attention-row-limit 10)
+ (out (org-drill-statistics--render-attention 'file))
+ (link-count
+ (cl-count ?\n
+ (mapconcat #'identity
+ (seq-filter
+ (lambda (l) (string-match-p "org-drill-card:" l))
+ (split-string out "\n"))
+ "\n"))))
+ (should (string-match-p "+2 more" out))
+ ;; 10 leech (capped) + 1 overdue + 1 forgotten = 12 link rows.
+ (should (= 12 (1+ link-count))))))
+
+(ert-deftest test-org-drill-statistics-attention-leech-sort-worst-first ()
+ "Leech rows are ordered by ascending average quality, worst first."
+ (with-temp-buffer
+ (test-org-drill-stats--attn-fixture)
+ (let* ((out (org-drill-statistics--render-attention 'file))
+ (leech-00 (string-match "Leech 00" out))
+ (leech-01 (string-match "Leech 01" out)))
+ (should leech-00)
+ (should leech-01)
+ ;; Leech 00 has avg 1.0, Leech 01 has 1.1, so 00 sorts first.
+ (should (< leech-00 leech-01)))))
+
+(ert-deftest test-org-drill-statistics-attention-empty-category-note ()
+ "A category with no matches renders a note rather than a table."
+ (with-temp-buffer
+ (insert "* Cards\n** Healthy :drill:\n:PROPERTIES:\n")
+ (insert ":DRILL_FAILURE_COUNT: 0\n:DRILL_TOTAL_REPEATS: 5\n:END:\n")
+ (org-mode)
+ (let ((out (org-drill-statistics--render-attention 'file)))
+ (should (string-match-p "No leech candidates\\." out))
+ (should (string-match-p "No long-overdue cards\\." out))
+ (should (string-match-p "No forgotten-new cards\\." out)))))
+
+(ert-deftest test-org-drill-statistics-attention-no-footer-under-cap ()
+ "With matches at or under the cap, no +N more footer appears."
+ (with-temp-buffer
+ (insert "* Cards\n")
+ (dotimes (i 3)
+ (insert (format "** Leech %d :drill:\n" i))
+ (insert ":PROPERTIES:\n")
+ (insert (format ":DRILL_FAILURE_COUNT: %d\n"
+ (+ org-drill-leech-failure-threshold 1)))
+ (insert ":DRILL_AVERAGE_QUALITY: 1.0\n:END:\n"))
+ (org-mode)
+ (let* ((org-drill-statistics-attention-row-limit 10)
+ (out (org-drill-statistics--render-attention 'file)))
+ (should-not (string-match-p "more" out)))))
+
+(ert-deftest test-org-drill-statistics-card-link-sanitizes-brackets ()
+ "Closing brackets in a heading cannot terminate the link early."
+ (let ((link (org-drill-statistics--card-link "a]] b" 42)))
+ (should (string-prefix-p "[[org-drill-card:42][" link))
+ (should (string-suffix-p "]]" link))
+ (should-not (string-match-p "a]] b" link))))
+
+(ert-deftest test-org-drill-statistics-card-link-empty-heading-fallback ()
+ "An empty heading falls back to a position-based description."
+ (let ((link (org-drill-statistics--card-link "" 99)))
+ (should (string-match-p "\\[\\[org-drill-card:99\\]\\[card at 99\\]\\]"
+ link))))
+
+(provide 'test-org-drill-statistics-render-attention)
+
+;;; test-org-drill-statistics-render-attention.el ends here