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-forecast.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-forecast.el')
| -rw-r--r-- | tests/test-org-drill-statistics-render-forecast.el | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/tests/test-org-drill-statistics-render-forecast.el b/tests/test-org-drill-statistics-render-forecast.el new file mode 100644 index 0000000..8001d68 --- /dev/null +++ b/tests/test-org-drill-statistics-render-forecast.el @@ -0,0 +1,131 @@ +;;; test-org-drill-statistics-render-forecast.el --- Tests for render-forecast statistics -*- lexical-binding: t; -*- + +;;; Commentary: +;; ERT tests for the org-drill statistics dashboard render-forecast block. + +;;; Code: + +(require 'ert) +(require 'org-drill) +(require 'cl-lib) +(require 'org) + +(defun test-org-drill-statistics--make-record + (start-day-offset &optional qualities) + "Build an `org-drill-session-record' START-DAY-OFFSET days from now. +QUALITIES defaults to a single passing quality. Helper for forecast +render tests, though the forecast itself reads org entries, not the log; +kept minimal and self-contained." + (let ((start (float-time + (time-add (current-time) + (days-to-time start-day-offset))))) + (make-org-drill-session-record + :start-time start + :end-time (+ start 60.0) + :scope 'file + :algorithm 'sm5 + :qualities (or qualities (vector 5)) + :pass-percent 100 + :new-count 0 + :mature-count 0 + :failed-count 0 + :cram-mode nil))) + +(ert-deftest test-org-drill-statistics-render-forecast-has-subheading () + "The rendered section starts with the Forecast subheading." + (cl-letf (((symbol-function 'org-drill-statistics--forecast) + (lambda (&rest _) '(0 0 0 0 0 0 0)))) + (let ((out (org-drill-statistics--render-forecast))) + (should (string-prefix-p "** Forecast\n" out))))) + +(ert-deftest test-org-drill-statistics-render-forecast-header-labels () + "The header row labels Today and the +1..+6 offsets in order." + (cl-letf (((symbol-function 'org-drill-statistics--forecast) + (lambda (&rest _) '(0 0 0 0 0 0 0)))) + (let ((out (org-drill-statistics--render-forecast))) + (should (string-match-p + "| Today | \\+1 | \\+2 | \\+3 | \\+4 | \\+5 | \\+6 |" + out))))) + +(ert-deftest test-org-drill-statistics-render-forecast-counts-row () + "The counts row reflects the forecast helper's per-day values." + (cl-letf (((symbol-function 'org-drill-statistics--forecast) + (lambda (&rest _) '(3 1 0 5 0 2 4)))) + (let ((out (org-drill-statistics--render-forecast))) + (should (string-match-p "| 3 | 1 | 0 | 5 | 0 | 2 | 4 |" out))))) + +(ert-deftest test-org-drill-statistics-render-forecast-trailing-newline () + "The section ends with a newline so sections concatenate cleanly." + (cl-letf (((symbol-function 'org-drill-statistics--forecast) + (lambda (&rest _) '(0 0 0 0 0 0 0)))) + (let ((out (org-drill-statistics--render-forecast))) + (should (string-suffix-p "\n" out))))) + +(ert-deftest test-org-drill-statistics-render-forecast-honors-days () + "A custom DAYS yields a header and row sized to the forecast length." + (cl-letf (((symbol-function 'org-drill-statistics--forecast) + (lambda (&optional _scope _days) '(2 7 1)))) + (let ((out (org-drill-statistics--render-forecast nil 3))) + (should (string-match-p "| Today | \\+1 | \\+2 |" out)) + (should (string-match-p "| 2 | 7 | 1 |" out)) + ;; No fourth column should appear. + (should-not (string-match-p "\\+3" out))))) + +(ert-deftest test-org-drill-statistics-render-forecast-empty-window () + "A zero-length forecast renders a note instead of a table." + (cl-letf (((symbol-function 'org-drill-statistics--forecast) + (lambda (&rest _) '()))) + (let ((out (org-drill-statistics--render-forecast nil 0))) + (should (string-prefix-p "** Forecast\n" out)) + (should (string-match-p "No forecast window configured." out)) + (should-not (string-match-p "|" out))))) + +(ert-deftest test-org-drill-statistics-render-forecast-single-day () + "A one-day forecast renders just the Today column." + (cl-letf (((symbol-function 'org-drill-statistics--forecast) + (lambda (&rest _) '(9))) + (org-drill-statistics-forecast-days 1)) + (let ((out (org-drill-statistics--render-forecast))) + (should (string-match-p "| Today |" out)) + (should (string-match-p "| 9 |" out)) + (should-not (string-match-p "\\+1" out))))) + +(ert-deftest test-org-drill-statistics-render-forecast-default-days () + "With no DAYS argument the section spans the configured default." + (let ((org-drill-statistics-forecast-days 7)) + (cl-letf (((symbol-function 'org-drill-statistics--forecast) + (lambda (&optional _scope days) + ;; Echo the resolved day count as a flat list so the + ;; renderer's column count is observable. + (make-list (or days org-drill-statistics-forecast-days) + 0)))) + (let ((out (org-drill-statistics--render-forecast))) + (should (string-match-p "\\+6 |" out)) + (should-not (string-match-p "\\+7" out)))))) + +(ert-deftest test-org-drill-statistics-render-forecast-with-scope-buffer () + "End to end through the real forecast helper against a temp org buffer. +Two cards are scheduled today and one is scheduled three days out; the +rendered counts row must reflect that bucketing." + (let ((today (format-time-string "%Y-%m-%d" (current-time))) + (plus3 (format-time-string + "%Y-%m-%d" (time-add (current-time) (days-to-time 3))))) + (with-temp-buffer + ;; Cards carry the drill tag and put SCHEDULED right after the + ;; heading so the traversal sees them; scope is 'file (a buffer + ;; list would be read as a list of file paths and fail). + (insert (format "* Drill cards\n")) + (insert (format "** Card one :drill:\nSCHEDULED: <%s>\n:PROPERTIES:\n:DRILL_CARD_TYPE: simple\n:END:\nfront\n" today)) + (insert (format "** Card two :drill:\nSCHEDULED: <%s>\n:PROPERTIES:\n:DRILL_CARD_TYPE: simple\n:END:\nfront\n" today)) + (insert (format "** Card three :drill:\nSCHEDULED: <%s>\n:PROPERTIES:\n:DRILL_CARD_TYPE: simple\n:END:\nfront\n" plus3)) + (org-mode) + (let* ((org-drill-scope 'file) + (org-drill-question-tag "drill") + (org-drill-match nil) + (out (org-drill-statistics--render-forecast 'file 7))) + ;; Today column = 2, +3 column = 1, others 0. + (should (string-match-p "| 2 | 0 | 0 | 1 | 0 | 0 | 0 |" out)))))) + +(provide 'test-org-drill-statistics-render-forecast) + +;;; test-org-drill-statistics-render-forecast.el ends here |
