diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-31 08:35:16 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-31 08:35:16 -0500 |
| commit | 26cc4472dea261a1ad13fbee8fb6a91b019f77bb (patch) | |
| tree | a7063337b05c3ea278a5b910d0f1420de033dfe8 /tests/test-org-drill-statistics-render-attention.el | |
| parent | 532ce532465834ce06238648ba1490c48bed29ca (diff) | |
| download | org-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.el | 141 |
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 |
