feat: add book detail view with spine-book-model, httpd/book handler, and title links
- Add spine--format-defs defconst shared between index and book models - Add spine-book-model with status extraction, format selector, notes, recommendation - Add templates/book.mustache (Pico CSS, journal-style layout) - Add httpd/book handler serving the detail page - Link book titles in index view to /book?id=X - Add spine-book-model-test.el with 13 tests
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
;;; 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.")
|
||||
|
||||
(defconst spine-book-model-test--aj
|
||||
(cl-find "3a1b-aj" (spine-books)
|
||||
:key (lambda (b) (plist-get b :id))
|
||||
:test #'equal)
|
||||
"Ancillary Justice fixture.")
|
||||
|
||||
(defconst spine-book-model-test--pir
|
||||
(cl-find "5e8d-pir" (spine-books)
|
||||
:key (lambda (b) (plist-get b :id))
|
||||
:test #'equal)
|
||||
"Piranesi fixture.")
|
||||
|
||||
(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 book has status_class reading."
|
||||
(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 book has status_class read."
|
||||
(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 book has status_class want."
|
||||
(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-author ()
|
||||
"Author is 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-meta-with-format-and-date ()
|
||||
"Meta includes format and date when both present."
|
||||
(let ((model (spine-book-model spine-book-model-test--uow)))
|
||||
(should (string-match "started.*audiobook" (ht-get model "meta")))))
|
||||
|
||||
(ert-deftest spine-book-model-format-active ()
|
||||
"Format list has correct active flag."
|
||||
(let* ((model (spine-book-model spine-book-model-test--uow))
|
||||
(formats (ht-get model "formats")))
|
||||
(should (= (length formats) 3))
|
||||
(should (equal (ht-get (nth 0 formats) "label") "Hardcover"))
|
||||
(should-not (ht-get (nth 0 formats) "active"))
|
||||
(should (equal (ht-get (nth 1 formats) "label") "eBook"))
|
||||
(should-not (ht-get (nth 1 formats) "active"))
|
||||
(should (equal (ht-get (nth 2 formats) "label") "Audiobook"))
|
||||
(should (ht-get (nth 2 formats) "active"))))
|
||||
|
||||
(ert-deftest spine-book-model-notes ()
|
||||
"Notes are mapped correctly."
|
||||
(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-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"))))
|
||||
|
||||
(ert-deftest spine-book-model-no-todo-prefix ()
|
||||
"Book with no TODO prefix shows title as-is and empty status."
|
||||
(let* ((books (spine-books))
|
||||
(tcm (cl-find "b2c1-tcm" books
|
||||
:key (lambda (b) (plist-get b :id))
|
||||
:test #'equal)))
|
||||
(should tcm)
|
||||
(let ((model (spine-book-model tcm)))
|
||||
(should (equal (ht-get model "title") "The Checklist Manifesto"))
|
||||
(should (equal (ht-get model "status_class") ""))
|
||||
(should (equal (ht-get model "status_label") "")))))
|
||||
|
||||
(ert-deftest spine-book-model-has-date-no-format ()
|
||||
"Has date but no format → meta shows started date only."
|
||||
(let* ((books (spine-books))
|
||||
(tcm (cl-find "b2c1-tcm" books
|
||||
:key (lambda (b) (plist-get b :id))
|
||||
:test #'equal)))
|
||||
(should tcm)
|
||||
(let ((model (spine-book-model tcm)))
|
||||
(should (equal (ht-get model "meta") "started 15 Jun")))))
|
||||
|
||||
(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"))))
|
||||
Reference in New Issue
Block a user