291 lines
14 KiB
EmacsLisp
Executable File
291 lines
14 KiB
EmacsLisp
Executable File
;;; build-site-tests.el --- ERT unit tests for build modules -*- lexical-binding: t; -*-
|
|
|
|
;; Run with:
|
|
;; emacs -Q --batch -l lisp/tests/build-site-tests.el -f ert-run-tests-batch-and-exit
|
|
;; Or interactively:
|
|
;; M-x load-file RET lisp/tests/build-site-tests.el RET
|
|
;; M-x ert RET t RET
|
|
|
|
;;; Code:
|
|
|
|
(defconst z/test-root
|
|
(file-name-directory
|
|
(directory-file-name
|
|
(file-name-directory
|
|
(directory-file-name
|
|
(file-name-directory (or load-file-name buffer-file-name)))))))
|
|
|
|
(defconst z/build-start-time (float-time))
|
|
(defconst z/site-root (file-name-as-directory z/test-root))
|
|
(defvaralias 'site-root 'z/site-root)
|
|
(defconst z/output-root (file-name-as-directory (expand-file-name "output/" z/site-root)))
|
|
|
|
(defun site-path (path)
|
|
"Expand PATH relative to test `z/site-root'."
|
|
(expand-file-name path z/site-root))
|
|
|
|
(defun output-path (path)
|
|
"Expand PATH relative to test `z/output-root'."
|
|
(expand-file-name path z/output-root))
|
|
|
|
(add-to-list 'load-path (site-path "lisp"))
|
|
|
|
(require 'ert)
|
|
(require 'cl-lib)
|
|
(require 'org)
|
|
(require 'build-core)
|
|
|
|
(z/configure-org)
|
|
(require 'recently-updated)
|
|
(require 'tags)
|
|
(require 'sidenotes)
|
|
(require 'sitemaps)
|
|
(require 'build-assets)
|
|
(require 'build-comments)
|
|
(require 'build-html)
|
|
(require 'build-lima)
|
|
|
|
;; ── 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 <section id=\"comments\"> 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 "<div>body</div>" 'html info)))
|
|
(should (string-match-p "id=\"comments\"" result))
|
|
(should (string-match-p "data-slug=\"my-slug\"" result))
|
|
(should (string-match-p "<div>body</div>" 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 "<div>body</div>" 'html info)))
|
|
(should (string= "<div>body</div>" result)))))
|
|
|
|
(ert-deftest test/insert-comments-ignores-non-html-backends ()
|
|
"For non-HTML backends the body should pass through unchanged."
|
|
(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 "<div>body</div>" 'latex info)))
|
|
(should (string= "<div>body</div>" 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 "<div>body</div>" 'html info)))
|
|
(should (string= "<div>body</div>" 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 "<div>body</div>" '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/no-sidenotes-file-p so they run without depending on
|
|
;; full site metadata.
|
|
|
|
(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/no-sidenotes-file-p) (lambda (_) t)))
|
|
(let* ((info (list :input-file temp-file))
|
|
(output "<html><body><p>hello</p></body></html>")
|
|
(result (z/org-html-add-body-classes output 'html info)))
|
|
(should (string-match-p "class=\"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/no-sidenotes-file-p) (lambda (_) nil)))
|
|
(let* ((info (list :input-file temp-file))
|
|
(output "<html><body><p>hello</p></body></html>")
|
|
(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 output should pass through unchanged."
|
|
(with-temp-org-file "#+TITLE: LaTeX\n\nContent.\n"
|
|
(cl-letf (((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)))
|
|
(should (string= output result))))))
|
|
|
|
;; ── Resource Loader SPA ─────────────────────────────────────────────────────
|
|
|
|
(ert-deftest test/resource-loader-assets-registered ()
|
|
"The Resource Loader's loaders should be present in the shared head."
|
|
(should (string-match-p "pages/resource-loader\\.css" z/shared-head))
|
|
(should (string-match-p "pages/resource-loader-model\\.js" z/shared-head))
|
|
(should (string-match-p "pages/resource-loader\\.js" z/shared-head)))
|
|
|
|
(ert-deftest test/resource-loader-publishes-with-site-shell ()
|
|
"The /rl source should publish with the normal preamble and postamble."
|
|
(let ((html
|
|
(with-temp-buffer
|
|
(insert-file-contents (site-path "rl/index.org"))
|
|
(setq-local buffer-file-name (site-path "rl/index.org"))
|
|
(org-mode)
|
|
(org-export-as
|
|
'z-html nil nil nil
|
|
(list :html-preamble z/preamble
|
|
:html-postamble z/postamble
|
|
:html-head z/shared-head
|
|
:with-toc nil
|
|
:section-numbers nil)))))
|
|
(should (string-match-p "id=\"resource-loader\"" html))
|
|
(should (string-match-p "class=\"banner-header\"" html))
|
|
(should (string-match-p "<footer>" html))))
|
|
|
|
;;; build-site-tests.el ends here
|