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-overview.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-overview.el')
| -rw-r--r-- | tests/test-org-drill-statistics-render-overview.el | 106 |
1 files changed, 106 insertions, 0 deletions
diff --git a/tests/test-org-drill-statistics-render-overview.el b/tests/test-org-drill-statistics-render-overview.el new file mode 100644 index 0000000..05cb166 --- /dev/null +++ b/tests/test-org-drill-statistics-render-overview.el @@ -0,0 +1,106 @@ +;;; test-org-drill-statistics-render-overview.el --- Tests for render-overview statistics -*- lexical-binding: t; -*- + +;;; Commentary: +;; ERT tests for the org-drill statistics dashboard render-overview block. + +;;; Code: + +(require 'ert) +(require 'org-drill) +(require 'cl-lib) +(require 'org) + +(defun test-org-drill-statistics--overview-record (start end qualities pass) + "Build a session record fixture for overview renderer tests. +START and END are floats; QUALITIES a vector of ints; PASS an int." + (make-org-drill-session-record + :start-time start + :end-time end + :scope 'file + :algorithm 'sm5 + :qualities qualities + :pass-percent pass + :new-count 0 + :mature-count 0 + :failed-count 0 + :cram-mode nil)) + +(ert-deftest test-org-drill-statistics-overview-table-row () + "Overview renders the header and a data row from the scope counts." + (cl-letf (((symbol-function 'org-drill-statistics--overview-counts) + (lambda (&optional _scope) + (list :total 42 :new 7 :mature 30 :lapsed 5)))) + (let ((out (org-drill-statistics--render-overview nil nil))) + (should (string-match-p "\\*\\* Overview" out)) + (should (string-match-p + "| Total cards | New | Mature | Lapsed |" out)) + (should (string-match-p "| 42 | 7 | 30 | 5 |" out))))) + +(ert-deftest test-org-drill-statistics-overview-last-session-recap () + "The recap line reports date, duration, card count, and pass percent." + (cl-letf (((symbol-function 'org-drill-statistics--overview-counts) + (lambda (&optional _scope) + (list :total 1 :new 1 :mature 0 :lapsed 0)))) + ;; 2026-05-15 12:00:00 local, 15 minutes long, 3 cards, 67% pass. + (let* ((start (float-time (encode-time 0 0 12 15 5 2026))) + (end (+ start (* 15 60))) + (record (test-org-drill-statistics--overview-record + start end (vector 4 4 1) 67)) + (out (org-drill-statistics--render-overview nil (list record)))) + (should (string-match-p "Last session: 2026-05-15" out)) + (should (string-match-p "15 min" out)) + (should (string-match-p "3 cards reviewed" out)) + (should (string-match-p "67% pass" out))))) + +(ert-deftest test-org-drill-statistics-overview-singular-card () + "A one-card session uses the singular \"card\" in the recap." + (cl-letf (((symbol-function 'org-drill-statistics--overview-counts) + (lambda (&optional _scope) + (list :total 1 :new 0 :mature 1 :lapsed 0)))) + (let* ((start (float-time (encode-time 0 0 9 1 1 2026))) + (end (+ start 60.0)) + (record (test-org-drill-statistics--overview-record + start end (vector 5) 100)) + (out (org-drill-statistics--render-overview nil (list record)))) + (should (string-match-p "1 card reviewed" out)) + (should-not (string-match-p "1 cards reviewed" out))))) + +(ert-deftest test-org-drill-statistics-overview-empty-log () + "With no logged sessions the recap states none recorded." + (cl-letf (((symbol-function 'org-drill-statistics--overview-counts) + (lambda (&optional _scope) + (list :total 0 :new 0 :mature 0 :lapsed 0)))) + ;; Bind the persistent log to empty so a nil LOG arg resolves to an + ;; empty log rather than falling back to whatever sessions the + ;; running Emacs has persisted. + (let* ((org-drill-session-log nil) + (out (org-drill-statistics--render-overview nil nil))) + (should (string-match-p "Last session: none recorded yet" out)) + (should (string-match-p "| 0 | 0 | 0 | 0 |" out))))) + +(ert-deftest test-org-drill-statistics-overview-scope-traversal () + "Counts come from the org buffer in scope via the real aggregator. +Components integrated: +- org-drill-statistics--render-overview (entry point, real) +- org-drill-statistics--overview-counts (real, traverses the buffer) +- org-drill-entry-status / org-drill-session (real) +Validates the renderer threads SCOPE through to a live org traversal +rather than relying on a stub." + (with-temp-buffer + ;; The card must carry the drill question tag, otherwise + ;; `org-drill-map-entries' skips it and the population is zero. + (insert "* Cards\n" + "** Card one :drill:\n" + ":PROPERTIES:\n:DRILL_CARD_TYPE: simple\n:END:\n" + "Front\n") + (org-mode) + (let* ((org-drill-scope 'file) + (org-drill-question-tag "drill") + (org-drill-match nil) + (out (org-drill-statistics--render-overview 'file nil))) + ;; One genuine drill card, never reviewed, so total and new are 1. + (should (string-match-p "| 1 | 1 | 0 | 0 |" out))))) + +(provide 'test-org-drill-statistics-render-overview) + +;;; test-org-drill-statistics-render-overview.el ends here |
