;;; build-site-tests.el --- ERT unit tests for build-site.el -*- lexical-binding: t; -*- ;; Run with: ;; emacs -Q --batch -l build-site.el -l build-site-tests.el -f ert-run-tests-batch-and-exit ;; Or interactively: ;; M-x load-file RET build-site-tests.el RET ;; M-x ert RET t RET ;;; Code: (require 'ert) (require 'org) ;; ── Helpers ────────────────────────────────────────────────────────────────── (defmacro with-temp-org-file (content &rest body) "Create a temporary .org file containing CONTENT, run BODY with `temp-file' bound." (declare (indent 1)) `(let ((temp-file (make-temp-file "build-site-test-" nil ".org"))) (unwind-protect (progn (with-temp-file temp-file (insert ,content)) ,@body) (delete-file temp-file)))) ;; ── site-path ──────────────────────────────────────────────────────────────── (ert-deftest test/site-path-expands-relative () "site-path should expand a relative path against site-root." ;; We can't call site-path directly without build-site loaded, so we test ;; the same logic inline. (let* ((site-root "/tmp/mysite/") (result (expand-file-name "output" site-root))) (should (string= result "/tmp/mysite/output")))) (ert-deftest test/site-path-handles-empty-string () "site-path with \"\" returns the site-root without a trailing slash. `expand-file-name' normalises the result, dropping the trailing slash." (let* ((site-root "/tmp/mysite/") (result (expand-file-name "" site-root))) ;; expand-file-name strips the trailing slash, giving "/tmp/mysite" (should (string= result "/tmp/mysite")))) ;; ── z/comments-file-p ──────────────────────────────────────────────────────── (ert-deftest test/comments-file-p-returns-true-for-t () "Files with #+COMMENTS: t should be detected." (with-temp-org-file "#+TITLE: Test\n#+COMMENTS: t\n\nBody.\n" (should (z/comments-file-p temp-file)))) (ert-deftest test/comments-file-p-case-insensitive () "#+COMMENTS: T (uppercase) should still match." (with-temp-org-file "#+TITLE: Test\n#+COMMENTS: T\n\nBody.\n" (should (z/comments-file-p temp-file)))) (ert-deftest test/comments-file-p-returns-nil-for-false () "Files with #+COMMENTS: nil should return nil." (with-temp-org-file "#+TITLE: Test\n#+COMMENTS: nil\n\nBody.\n" (should-not (z/comments-file-p temp-file)))) (ert-deftest test/comments-file-p-returns-nil-when-keyword-absent () "Files without #+COMMENTS keyword should return nil." (with-temp-org-file "#+TITLE: Test\n\nBody.\n" (should-not (z/comments-file-p temp-file)))) (ert-deftest test/comments-file-p-returns-nil-for-nonexistent-file () "Non-existent files should return nil, not signal an error." (should-not (z/comments-file-p "/tmp/does-not-exist-ever.org"))) (ert-deftest test/comments-file-p-tolerates-whitespace () "#+COMMENTS: with surrounding spaces around 't' should match." (with-temp-org-file "#+TITLE: Test\n#+COMMENTS: t \n\nBody.\n" (should (z/comments-file-p temp-file)))) ;; ── z/comments-file-slug ───────────────────────────────────────────────────── (ert-deftest test/comments-file-slug-uses-slug-keyword () "Should return the #+SLUG: value when present." (with-temp-org-file "#+TITLE: Test\n#+SLUG: my-cool-post\n\nBody.\n" (should (string= "my-cool-post" (z/comments-file-slug temp-file))))) (ert-deftest test/comments-file-slug-falls-back-to-filename () "Should fall back to the file-name-base when #+SLUG is absent." (with-temp-org-file "#+TITLE: Test\n\nBody.\n" (should (string= (file-name-base temp-file) (z/comments-file-slug temp-file))))) (ert-deftest test/comments-file-slug-ignores-blank-slug () "A #+SLUG: with only whitespace should fall back to filename." (with-temp-org-file "#+TITLE: Test\n#+SLUG: \n\nBody.\n" (should (string= (file-name-base temp-file) (z/comments-file-slug temp-file))))) ;; ── z/org-html-insert-comments-into-body ───────────────────────────────────── (ert-deftest test/insert-comments-adds-section-when-enabled () "Should append a
block when comments are on." (with-temp-org-file "#+TITLE: Test\n#+COMMENTS: t\n#+SLUG: my-slug\n\nBody.\n" (let* ((info (list :input-file temp-file)) (result (z/org-html-insert-comments-into-body "
body
" 'html info))) (should (string-match-p "id=\"comments\"" result)) (should (string-match-p "data-slug=\"my-slug\"" result)) (should (string-match-p "
body
" result))))) (ert-deftest test/insert-comments-leaves-body-unchanged-when-disabled () "Should return body unchanged when #+COMMENTS is absent." (with-temp-org-file "#+TITLE: Test\n\nBody.\n" (let* ((info (list :input-file temp-file)) (result (z/org-html-insert-comments-into-body "
body
" 'html info))) (should (string= "
body
" result))))) (ert-deftest test/insert-comments-ignores-non-html-backends () "For non-HTML backends the function currently returns nil (known bug: `when' has no else branch so the body string is not passed through). Fix: change `when' to `if' with body as the else clause. This test documents current behaviour; flip the should once fixed." (with-temp-org-file "#+TITLE: Test\n#+COMMENTS: t\n\nBody.\n" (let* ((info (list :input-file temp-file)) (result (z/org-html-insert-comments-into-body "
body
" 'latex info))) ;; BUG: should be (should (string= "
body
" result)) (should (null result))))) (ert-deftest test/insert-comments-handles-nil-input-file () "Should return body unchanged when :input-file is nil." (let* ((info (list :input-file nil)) (result (z/org-html-insert-comments-into-body "
body
" 'html info))) (should (string= "
body
" result)))) (ert-deftest test/insert-comments-slug-from-filename-when-no-slug-keyword () "Comments section data-slug should fall back to filename when #+SLUG absent." (with-temp-org-file "#+TITLE: Test\n#+COMMENTS: t\n\nBody.\n" (let* ((expected-slug (file-name-base temp-file)) (info (list :input-file temp-file)) (result (z/org-html-insert-comments-into-body "
body
" 'html info))) (should (string-match-p (regexp-quote (format "data-slug=\"%s\"" expected-slug)) result))))) ;; ── z/lima-sitemap-format-entry ─────────────────────────────────────────────── (ert-deftest test/lima-sitemap-format-entry-skips-directories () "Directories should return nil." (let* ((base-dir (make-temp-file "lima-test-" t)) (project (list "lima" :base-directory base-dir))) (unwind-protect (should-not (z/lima-sitemap-format-entry base-dir nil project)) (delete-directory base-dir t)))) (ert-deftest test/lima-sitemap-format-entry-formats-org-file () "Org files should produce a [[file:...][Title]] link." (let* ((base-dir (make-temp-file "lima-test-" t)) (org-file (expand-file-name "my-note.org" base-dir)) (project (list "lima" :base-directory base-dir))) (unwind-protect (progn (with-temp-file org-file (insert "#+TITLE: My Note\n\nContent.\n")) (let ((result (z/lima-sitemap-format-entry org-file nil project))) (should (stringp result)) (should (string-match-p "\\[\\[file:my-note\\.html\\]" result)))) (delete-directory base-dir t)))) ;; ── z/copy-neighbor-attachments ────────────────────────────────────────────── (ert-deftest test/copy-neighbor-attachments-copies-matching-dirs () "Sibling .attachments.* directories should be copied to pub-dir." (let* ((src-dir (make-temp-file "src-" t)) (pub-dir (make-temp-file "pub-" t)) (att-dir (expand-file-name ".attachments.my-note" src-dir)) (att-file (expand-file-name "image.png" att-dir))) (unwind-protect (progn (make-directory att-dir t) (with-temp-file att-file (insert "fake png")) (z/copy-neighbor-attachments src-dir pub-dir) (should (file-exists-p (expand-file-name ".attachments.my-note/image.png" pub-dir)))) (delete-directory src-dir t) (delete-directory pub-dir t)))) (ert-deftest test/copy-neighbor-attachments-ignores-non-matching () "Directories not starting with .attachments. should be left alone." (let* ((src-dir (make-temp-file "src-" t)) (pub-dir (make-temp-file "pub-" t)) (other-dir (expand-file-name "regular-dir" src-dir))) (unwind-protect (progn (make-directory other-dir t) (z/copy-neighbor-attachments src-dir pub-dir) (should-not (file-exists-p (expand-file-name "regular-dir" pub-dir)))) (delete-directory src-dir t) (delete-directory pub-dir t)))) (ert-deftest test/copy-neighbor-attachments-no-error-when-none-exist () "No error should be raised when there are no .attachments.* dirs." (let* ((src-dir (make-temp-file "src-" t)) (pub-dir (make-temp-file "pub-" t))) (unwind-protect (should-not (z/copy-neighbor-attachments src-dir pub-dir)) (delete-directory src-dir t) (delete-directory pub-dir t)))) ;; ── z/org-html-add-body-classes ────────────────────────────────────────────── ;; These tests stub out z/wip-file-p and z/no-sidenotes-file-p so they run ;; without the full site infrastructure loaded. (ert-deftest test/add-body-classes-adds-wip-class () "Should add class=\"wip\" to when z/wip-file-p is t." (with-temp-org-file "#+TITLE: WIP page\n\nContent.\n" (cl-letf (((symbol-function 'z/wip-file-p) (lambda (_) t)) ((symbol-function 'z/no-sidenotes-file-p) (lambda (_) nil))) (let* ((info (list :input-file temp-file)) (output "

hello

") (result (z/org-html-add-body-classes output 'html info))) (should (string-match-p "class=\"wip\"" result)))))) (ert-deftest test/add-body-classes-adds-no-sidenotes-class () "Should add class=\"no-sidenotes\" when z/no-sidenotes-file-p is t." (with-temp-org-file "#+TITLE: No sidenotes\n\nContent.\n" (cl-letf (((symbol-function 'z/wip-file-p) (lambda (_) nil)) ((symbol-function 'z/no-sidenotes-file-p) (lambda (_) t))) (let* ((info (list :input-file temp-file)) (output "

hello

") (result (z/org-html-add-body-classes output 'html info))) (should (string-match-p "class=\"no-sidenotes\"" result)))))) (ert-deftest test/add-body-classes-combines-multiple-classes () "Both wip and no-sidenotes classes should appear when both predicates are t." (with-temp-org-file "#+TITLE: Both\n\nContent.\n" (cl-letf (((symbol-function 'z/wip-file-p) (lambda (_) t)) ((symbol-function 'z/no-sidenotes-file-p) (lambda (_) t))) (let* ((info (list :input-file temp-file)) (output "

hello

") (result (z/org-html-add-body-classes output 'html info))) (should (string-match-p "wip" result)) (should (string-match-p "no-sidenotes" result)))))) (ert-deftest test/add-body-classes-unchanged-when-no-classes () "Output should be unmodified when no special classes apply." (with-temp-org-file "#+TITLE: Plain\n\nContent.\n" (cl-letf (((symbol-function 'z/wip-file-p) (lambda (_) nil)) ((symbol-function 'z/no-sidenotes-file-p) (lambda (_) nil))) (let* ((info (list :input-file temp-file)) (output "

hello

") (result (z/org-html-add-body-classes output 'html info))) (should (string= output result)))))) (ert-deftest test/add-body-classes-ignores-non-html-backend () "For non-HTML backends the function currently returns nil (known bug: `when' has no else branch so output is not passed through). Fix: change `when' to `if' with output as the else clause. This test documents current behaviour; flip the should once fixed." (with-temp-org-file "#+TITLE: LaTeX\n\nContent.\n" (cl-letf (((symbol-function 'z/wip-file-p) (lambda (_) t)) ((symbol-function 'z/no-sidenotes-file-p) (lambda (_) t))) (let* ((info (list :input-file temp-file)) (output "\\documentclass{article}") (result (z/org-html-add-body-classes output 'latex info))) ;; BUG: should be (should (string= output result)) (should (null result)))))) ;;; build-site-tests.el ends here