;;; spine-book-model-test.el — ERT tests for spine-book-model (require 'ert) (require 'cl-lib) (setq spine-skip-server-start t) (setq spine-org-file (expand-file-name "sample-books.org" (file-name-directory (directory-file-name (file-name-directory load-file-name))))) (load-file (expand-file-name "../spine.el" (file-name-directory load-file-name))) (defconst spine-book-model-test--uow (cl-find "8c1e-uow" (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal) "Use of Weapons fixture (READING, audiobook, tags=scifi+literary, rec_by=Priya).") (defconst spine-book-model-test--aj (cl-find "3a1b-aj" (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal) "Ancillary Justice fixture (READ, ebook, rating=5, tags=scifi).") (defconst spine-book-model-test--pir (cl-find "5e8d-pir" (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal) "Piranesi fixture (WANT, no format, no tags, no rec).") (defconst spine-book-model-test--tcm (let* ((books (spine-books)) (b (cl-find "b2c1-tcm" books :key (lambda (x) (plist-get x :id)) :test #'equal))) b) "The Checklist Manifesto fixture (no TODO, no format, no rec).") ;; --- Title & status ---------------------------------------------------- (ert-deftest spine-book-model-title-strips-todo () "Title has TODO prefix stripped." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "title") "Use of Weapons")))) (ert-deftest spine-book-model-status-reading () "READING status_class and label." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "status_class") "reading")) (should (equal (ht-get model "status_label") "Reading")))) (ert-deftest spine-book-model-status-read () "READ status_class and label." (let ((model (spine-book-model spine-book-model-test--aj))) (should (equal (ht-get model "status_class") "read")) (should (equal (ht-get model "status_label") "Read")))) (ert-deftest spine-book-model-status-want () "WANT status_class and label." (let ((model (spine-book-model spine-book-model-test--pir))) (should (equal (ht-get model "status_class") "want")) (should (equal (ht-get model "status_label") "Want")))) (ert-deftest spine-book-model-no-todo-prefix () "No TODO prefix shows title as-is and empty status." (let ((model (spine-book-model spine-book-model-test--tcm))) (should (equal (ht-get model "title") "The Checklist Manifesto")) (should (equal (ht-get model "status_class") "")) (should (equal (ht-get model "status_label") "")))) ;; --- Metadata fields ---------------------------------------------------- (ert-deftest spine-book-model-author () "Author passed through." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "author") "Iain M. Banks")))) (ert-deftest spine-book-model-shelf-name () "Shelf_name passed through." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "shelf_name") "Fiction")))) (ert-deftest spine-book-model-format-fields () "Format icon and label match the book's format." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "format_icon") "ti-headphones")) (should (equal (ht-get model "format_label") "Audiobook")))) (ert-deftest spine-book-model-format-none () "No format yields empty label and default icon." (let ((model (spine-book-model spine-book-model-test--pir))) (should (equal (ht-get model "format_icon") "ti-book")) (should (equal (ht-get model "format_label") "")))) (ert-deftest spine-book-model-tags-display () "Tags rendered as HTML badge spans." (let ((model (spine-book-model spine-book-model-test--uow))) (let ((tags (ht-get model "tags_display"))) (should (string-match "scifi" tags)) (should (string-match "literary" tags))))) (ert-deftest spine-book-model-no-tags () "No tags yields empty string." (let ((model (spine-book-model spine-book-model-test--tcm))) (should (equal (ht-get model "tags_display") "")))) (ert-deftest spine-book-model-started-display () "started_display from ADDED property." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "started_display") "20 Feb 2026")))) (ert-deftest spine-book-model-added-display () "added_display from ADDED property." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "added_display") "20 Feb 2026")))) (ert-deftest spine-book-model-finished-default () "finished_display defaults to em-dash." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "finished_display") "—")))) (ert-deftest spine-book-model-rating-display () "Rating produces star characters." (let ((model (spine-book-model spine-book-model-test--aj))) (should (equal (ht-get model "rating_display") "★★★★★")) (should (ht-get model "has_rating")))) (ert-deftest spine-book-model-rating-none () "No rating shows placeholder text." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "rating_display") "rate when finished")) (should-not (ht-get model "has_rating")))) ;; --- Format/status/shelf options --------------------------------------- (ert-deftest spine-book-model-status-options () "status_options includes all statuses with one selected." (let* ((model (spine-book-model spine-book-model-test--uow)) (opts (ht-get model "status_options"))) (should (= (length opts) 3)) (should (equal (ht-get (nth 0 opts) "name") "Want")) (should-not (ht-get (nth 0 opts) "selected")) (should (equal (ht-get (nth 1 opts) "name") "Reading")) (should (ht-get (nth 1 opts) "selected")) (should (equal (ht-get (nth 2 opts) "name") "Read")) (should-not (ht-get (nth 2 opts) "selected")))) (ert-deftest spine-book-model-shelf-options () "shelf_options lists all shelves with current one selected." (let* ((model (spine-book-model spine-book-model-test--uow)) (opts (ht-get model "shelf_options"))) (should (>= (length opts) 3)) (let ((fiction (cl-find "Fiction" opts :key (lambda (o) (ht-get o "name")) :test #'equal))) (should fiction) (should (ht-get fiction "selected"))) (let ((scifi (cl-find "Science Fiction" opts :key (lambda (o) (ht-get o "name")) :test #'equal))) (should scifi) (should-not (ht-get scifi "selected"))))) ;; --- Notes ------------------------------------------------------------- (ert-deftest spine-book-model-notes () "Notes mapped with date and text." (let* ((model (spine-book-model spine-book-model-test--uow)) (notes (ht-get model "notes"))) (should (= (length notes) 3)) (should (equal (ht-get (nth 0 notes) "date") "2026-03-02")) (should (equal (ht-get (nth 0 notes) "text") "The two-track structure is doing something I can't name yet.")))) (ert-deftest spine-book-model-notes-empty () "No notes yields empty array." (let* ((model (spine-book-model spine-book-model-test--tcm)) (notes (ht-get model "notes"))) (should (listp notes)) (should (= (length notes) 0)))) ;; --- Recommendation ---------------------------------------------------- (ert-deftest spine-book-model-recommendation () "Recommendation includes initials, by, and note." (let* ((model (spine-book-model spine-book-model-test--uow)) (rec (ht-get model "recommendation"))) (should rec) (should (equal (ht-get rec "initials") "P")) (should (equal (ht-get rec "by") "Priya")) (should (string-match "Player of Games" (ht-get rec "note"))))) (ert-deftest spine-book-model-no-recommendation () "No recommendation when rec_by absent." (let ((model (spine-book-model spine-book-model-test--aj))) (should-not (ht-get model "recommendation")))) ;; --- Navigation -------------------------------------------------------- (ert-deftest spine-book-model-back-url () "back_url links to index with this book's id." (let ((model (spine-book-model spine-book-model-test--uow))) (should (equal (ht-get model "back_url") "/index?id=8c1e-uow"))))