seam-test.el (19622B)
1 ;;; seam-test.el --- Tests for Seam -*- lexical-binding: t -*- 2 3 ;; Copyright (C) 2025 Spencer Williams 4 5 ;; Author: Spencer Williams <spnw@plexwave.org> 6 7 ;; SPDX-License-Identifier: GPL-3.0-or-later 8 9 ;; This file is not part of GNU Emacs. 10 11 ;; This program is free software: you can redistribute it and/or modify 12 ;; it under the terms of the GNU General Public License as published by 13 ;; the Free Software Foundation, either version 3 of the License, or 14 ;; (at your option) any later version. 15 16 ;; This program is distributed in the hope that it will be useful, 17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 ;; GNU General Public License for more details. 20 21 ;; You should have received a copy of the GNU General Public License 22 ;; along with this program. If not, see <http://www.gnu.org/licenses/>. 23 24 ;;; Commentary: 25 26 ;; Tests for Seam. 27 28 ;;; Code: 29 30 ;;; FIXME: Tests can fail if certain buffer names are already in use 31 ;;; on the test runner's system. 32 33 (require 'seam) 34 (require 'seam-export) 35 (require 'ert) 36 (require 'cl-lib) 37 38 (defvar seam-test-directory nil) 39 40 (defmacro seam-test-environment (&rest body) 41 (declare (indent 0)) 42 `(let* ((seam-test-directory (file-name-as-directory (make-temp-file "seam-test" t))) 43 (seam-note-directory seam-test-directory) 44 (default-directory seam-test-directory) 45 (seam-create-as-draft nil) 46 (seam-note-types '("private" "public")) 47 (seam-default-note-type "private") 48 (seam-title-formatter (lambda (title _type _draft-p) title)) 49 (seam-export-template-file nil) 50 (seam-export-template-string seam-export-default-template-string) 51 (seam-export-template-values nil) 52 (seam-export-internal-link-class nil) 53 (seam-export-alist 54 `((,(file-name-concat seam-test-directory "html") 55 :types ("public") 56 :root-path "/"))) 57 ;; Manually install hooks in test directory. 58 (dir-locals-directory-cache 59 (cons (list seam-test-directory 'seam-note-directory nil) 60 dir-locals-directory-cache))) 61 (unwind-protect 62 (progn ,@body) 63 (delete-directory seam-test-directory t)))) 64 65 (defmacro seam-test-with-notes (options varlist &rest body) 66 (declare (indent 2)) 67 `(seam-test-environment 68 (let ,options 69 (let ,(cl-loop for (name . args) in varlist 70 collect `(,name (seam-create-note ,@args))) 71 ;; FIXME: It's quite possible for tests to fail in such a way 72 ;; that this does not kill the buffers. 73 (unwind-protect (progn ,@body) 74 (mapcar #'kill-buffer (list ,@(mapcar #'car varlist)))))))) 75 76 (defun seam-test-strip-testdir (filename) 77 (string-remove-prefix seam-test-directory filename)) 78 79 (defun seam-test-list-files () 80 (mapcar 81 #'seam-test-strip-testdir 82 (directory-files-recursively seam-test-directory ""))) 83 84 (defun seam-test-add-contents (buffer contents) 85 (with-current-buffer buffer 86 (insert contents) 87 (insert "\n") 88 (save-buffer))) 89 90 (defun seam-test-replace-contents (buffer contents) 91 (with-current-buffer buffer 92 (erase-buffer) 93 (insert contents) 94 (insert "\n") 95 (save-buffer))) 96 97 (defun seam-test-link-to-buffer (buffer) 98 (format "[[seam:%s]]" (file-name-base (buffer-file-name buffer)))) 99 100 (defun seam-test-links-from-html (file) 101 (with-temp-buffer 102 (insert-file-contents file) 103 (delete-dups 104 (cl-loop for ret = (re-search-forward "<a href=\"/\\(.*\\)?\">" nil t) 105 while ret collect (match-string 1))))) 106 107 (ert-deftest seam-test-create-note-private () 108 (should 109 (equal 110 '("private/note.org") 111 (seam-test-with-notes () 112 ((note "Note")) 113 (seam-test-list-files))))) 114 115 (ert-deftest seam-test-create-note-public () 116 (should 117 (equal 118 '("html/note.html" "public/note.org") 119 (seam-test-with-notes () 120 ((note "Note" "public")) 121 (seam-test-list-files))))) 122 123 (ert-deftest seam-test-create-note-weird-filename () 124 (should 125 (equal 126 '("./Weir'd file name!" ("private/weird-file-name.org")) 127 (seam-test-with-notes () 128 ((weird "./Weir'd file name! ")) 129 (list (buffer-name weird) 130 (seam-test-list-files)))))) 131 132 (ert-deftest seam-test-get-title-from-buffer () 133 (seam-test-with-notes () ((note "Note")) 134 (should 135 (equal 136 (buffer-name note) 137 (seam-get-title-from-buffer note))))) 138 139 (ert-deftest seam-test-get-title-from-buffer-narrowed () 140 (should 141 (equal 142 "foo" 143 (seam-test-with-notes () 144 ((foo "foo")) 145 (with-current-buffer foo 146 (seam-test-add-contents foo "* My headline") 147 (forward-line) 148 (org-narrow-to-subtree) 149 (seam-get-title-from-buffer)))))) 150 151 (ert-deftest seam-test-get-title-from-file () 152 (seam-test-with-notes () ((note "Note")) 153 (should 154 (equal 155 (buffer-name note) 156 (seam-get-title-from-file (buffer-file-name note)))))) 157 158 (ert-deftest seam-test-create-note-invalid-type () 159 (should-error 160 (seam-test-environment 161 (kill-buffer (seam-create-note "Note" "invalid-type"))))) 162 163 (ert-deftest seam-test-create-note-name-conflict () 164 (should-error 165 (seam-test-environment 166 (kill-buffer (seam-create-note " Note 1 ")) 167 (kill-buffer (seam-create-note "Note_1"))))) 168 169 (ert-deftest seam-test-create-note-name-conflict-different-types () 170 (should-error 171 (seam-test-environment 172 (kill-buffer (seam-create-note "Note")) 173 (kill-buffer (seam-create-note "Note" "public"))))) 174 175 (ert-deftest seam-test-rename-note () 176 (should 177 (equal 178 '("New name" ("private/new-name.org")) 179 (seam-test-with-notes () ((note "Note")) 180 (with-current-buffer note 181 (erase-buffer) 182 (insert "* New name\n") 183 (save-buffer) 184 (list (buffer-name) (seam-test-list-files))))))) 185 186 (ert-deftest seam-test-rename-conflict () 187 (should-error 188 (seam-test-with-notes () 189 (let ((note1 "Note 1") 190 (note2 "Note 2")) 191 (with-current-buffer note2 192 (erase-buffer) 193 (insert "* note_1!\n") 194 (unwind-protect 195 (save-buffer) 196 (set-buffer-modified-p nil))))))) 197 198 (ert-deftest seam-test-buffer-name-format-default () 199 (should 200 (equal 201 "Note (private)" 202 (seam-test-with-notes ((seam-title-formatter #'seam-format-title-default)) 203 ((note "Note")) 204 (buffer-name note))))) 205 206 (ert-deftest seam-test-buffer-name-format-custom () 207 (should 208 (equal 209 "[private draft] Note" 210 (seam-test-with-notes ((seam-title-formatter 211 (lambda (title type draft-p) 212 (format "[%s%s] %s" type (if draft-p " draft" "") title)))) 213 ((note "Note" nil nil t)) 214 (buffer-name note))))) 215 216 (ert-deftest seam-test-link-update () 217 "Test that renaming a note updates its HTML and that of notes 218 which link to it." 219 (should 220 (equal '(("qux.html") 221 ("public/qux.org") 222 ("html/foo.html" "html/qux.html" "public/foo.org" "public/qux.org")) 223 (seam-test-with-notes () 224 ((foo "foo" "public") 225 (bar "bar" "public")) 226 (progn 227 (seam-test-add-contents foo (seam-test-link-to-buffer bar)) 228 (seam-test-replace-contents bar "* qux") 229 (list 230 (seam-test-links-from-html "html/foo.html") 231 (mapcar #'seam-test-strip-testdir (seam-get-links-from-file (buffer-file-name foo))) 232 (seam-test-list-files))))))) 233 234 (ert-deftest seam-test-link-update-no-unnecessary-export () 235 "Test that updating the contents of a note does not unnecessarily 236 re-export note to which it links." 237 (should-not 238 (member 239 "html/bar.html" 240 (seam-test-with-notes () 241 ((foo "foo" "public") 242 (bar "bar" "public")) 243 (seam-test-add-contents foo (seam-test-link-to-buffer bar)) 244 (delete-file "html/bar.html") 245 (seam-test-add-contents foo "hello") 246 (seam-test-list-files))))) 247 248 (ert-deftest seam-test-link-to-private () 249 "Test that a private link does not get exported in HTML." 250 (should-error 251 (seam-test-with-notes () 252 ((foo "foo" "public") 253 (bar "bar")) 254 (seam-test-add-contents foo (seam-test-link-to-buffer bar)) 255 (with-temp-buffer 256 (insert-file-contents "html/foo.html") 257 (buffer-string) 258 (re-search-forward "<a href=\"/bar.html\">"))))) 259 260 (ert-deftest seam-test-link-no-extension () 261 "Test that the :no-extension option causes links to render without 262 .html extension." 263 (should 264 (identity 265 (seam-test-with-notes ((seam-export-alist 266 `((,(file-name-concat seam-test-directory "html") 267 :types ("public") 268 :root-path "/" 269 :no-extension t)))) 270 ((foo "foo" "public") 271 (bar "bar" "public")) 272 (seam-test-add-contents foo (seam-test-link-to-buffer bar)) 273 (with-temp-buffer 274 (insert-file-contents "html/foo.html") 275 (buffer-string) 276 (re-search-forward "<a href=\"/bar\">")))))) 277 278 (ert-deftest seam-test-link-internal-class () 279 "Test that setting `seam-export-internal-link-class' correctly 280 renders the class." 281 (should 282 (identity 283 (seam-test-with-notes ((seam-export-internal-link-class "internal")) 284 ((foo "foo" "public") 285 (bar "bar" "public")) 286 (seam-test-add-contents foo (seam-test-link-to-buffer bar)) 287 (with-temp-buffer 288 (insert-file-contents "html/foo.html") 289 (buffer-string) 290 (re-search-forward "<a href=\"/bar.html\" class=\"internal\">")))))) 291 292 (ert-deftest seam-test-link-getters () 293 (should 294 (equal 295 '(("private/bar.org" "private/qux.org") 296 ("private/foo.org") 297 ("private/bar.org" "private/foo.org")) 298 (seam-test-with-notes () 299 ((foo "foo") 300 (bar "bar") 301 (qux "qux")) 302 (seam-test-add-contents foo (seam-test-link-to-buffer bar)) 303 (seam-test-add-contents foo (seam-test-link-to-buffer qux)) 304 (seam-test-add-contents bar (seam-test-link-to-buffer qux)) 305 (list 306 (mapcar #'seam-test-strip-testdir (seam-get-links-from-file (buffer-file-name foo))) 307 (mapcar #'seam-test-strip-testdir (seam-get-links-to-file (buffer-file-name bar))) 308 (mapcar #'seam-test-strip-testdir (seam-get-links-to-file (buffer-file-name qux)))))))) 309 310 (ert-deftest seam-test-delete-note () 311 "Test that deleting a note also deletes its HTML and re-exports 312 linking notes such that they no longer link to it." 313 (should 314 (equal 315 '(nil ("html/foo.html" "public/foo.org")) 316 (seam-test-with-notes () 317 ((foo "foo" "public") 318 (bar "bar" "public")) 319 (with-current-buffer foo 320 (seam-test-add-contents foo (seam-test-link-to-buffer bar))) 321 (let ((delete-by-moving-to-trash nil)) 322 (seam-delete-note (buffer-file-name bar))) 323 (list 324 (seam-test-links-from-html "html/foo.html") 325 (seam-test-list-files)))))) 326 327 (ert-deftest seam-test-backlinks-public () 328 "Test that linking to a note from a public note creates a 329 backlink." 330 (should 331 (identity 332 (seam-test-with-notes ((seam-export-template-string "{{{backlinks}}}")) 333 ((foo "foo" "public") 334 (bar "bar" "public")) 335 (with-current-buffer foo 336 (seam-test-add-contents foo (seam-test-link-to-buffer bar))) 337 (with-temp-buffer 338 (insert-file-contents "html/bar.html") 339 (re-search-forward "<a href=\"/foo.html\">")))))) 340 341 (ert-deftest seam-test-backlinks-private () 342 "Test that linking to a note from a private note does not create a 343 backlink." 344 (should 345 (equal 346 "" 347 (seam-test-with-notes ((seam-export-template-string "{{{backlinks}}}")) 348 ((foo "foo") 349 (bar "bar" "public")) 350 (with-current-buffer foo 351 (seam-test-add-contents foo (seam-test-link-to-buffer bar))) 352 (with-temp-buffer 353 (insert-file-contents "html/bar.html") 354 (buffer-string)))))) 355 356 (ert-deftest seam-test-backlinks-delete () 357 "Test that deleting a note removes backlink." 358 (should 359 (equal 360 "" 361 (seam-test-with-notes ((seam-export-template-string "{{{backlinks}}}")) 362 ((foo "foo" "public") 363 (bar "bar" "public")) 364 (with-current-buffer foo 365 (seam-test-add-contents foo (seam-test-link-to-buffer bar))) 366 (let ((delete-by-moving-to-trash nil)) 367 (seam-delete-note (buffer-file-name foo))) 368 (with-temp-buffer 369 (insert-file-contents "html/bar.html") 370 (buffer-string)))))) 371 372 (ert-deftest seam-test-backlinks-draft () 373 "Test that linking to a note from a draft note does not create a 374 backlink." 375 (should 376 (equal 377 "" 378 (seam-test-with-notes ((seam-export-template-string "{{{backlinks}}}")) 379 ((foo "foo" "public" nil t) 380 (bar "bar" "public")) 381 (with-current-buffer foo 382 (seam-test-add-contents foo (seam-test-link-to-buffer bar))) 383 (with-temp-buffer 384 (insert-file-contents "html/bar.html") 385 (buffer-string)))))) 386 387 (ert-deftest seam-test-set-type-private () 388 "Test that setting a public note to private will delete its HTML 389 file and update linking HTML files such that they no longer link 390 to it." 391 (should 392 (equal 393 '(nil ("html/foo.html" "private/bar.org" "public/foo.org")) 394 (seam-test-with-notes () 395 ((foo "foo" "public") 396 (bar "bar" "public")) 397 (with-current-buffer foo 398 (seam-test-add-contents foo (seam-test-link-to-buffer bar))) 399 (seam-set-note-type (buffer-file-name bar) "private") 400 (list 401 (seam-test-links-from-html "html/foo.html") 402 (seam-test-list-files)))))) 403 404 (ert-deftest seam-test-set-type-public () 405 "Test that setting a private note to public will export its HTML file and 406 update linking HTML files such that they link to it." 407 (should 408 (equal 409 '(("bar.html") 410 ("html/bar.html" "html/foo.html" "public/bar.org" "public/foo.org")) 411 (seam-test-with-notes () 412 ((foo "foo" "public") 413 (bar "bar")) 414 (with-current-buffer foo 415 (seam-test-add-contents foo (seam-test-link-to-buffer bar))) 416 (seam-set-note-type (buffer-file-name bar) "public") 417 (list 418 (seam-test-links-from-html "html/foo.html") 419 (seam-test-list-files)))))) 420 421 (ert-deftest seam-test-set-type-invalid () 422 "Test that setting a note to an invalid type raises an error." 423 (should-error 424 (seam-test-with-notes () 425 ((foo "foo")) 426 (seam-set-note-type (buffer-file-name foo) "invalid-type")))) 427 428 (ert-deftest seam-test-create-draft () 429 (should 430 (equal 431 '("public/-note.org") 432 (seam-test-with-notes ((seam-create-as-draft t)) 433 ((note "Note" "public")) 434 (seam-test-list-files))))) 435 436 (ert-deftest seam-test-create-draft-override () 437 (should 438 (equal 439 '("public/-note.org") 440 (seam-test-with-notes ((seam-note-types 441 '(("public" :create-as-draft t)))) 442 ((note "Note" "public")) 443 (seam-test-list-files))))) 444 445 (ert-deftest seam-test-set-draft () 446 "Test that toggling a note from non-draft to draft will delete its 447 HTML file and update linking HTML files such that they no longer 448 link to it." 449 (should 450 (equal 451 '(nil ("html/foo.html" "public/-bar.org" "public/foo.org")) 452 (seam-test-with-notes () 453 ((foo "foo" "public") 454 (bar "bar" "public")) 455 (with-current-buffer foo 456 (seam-test-add-contents foo (seam-test-link-to-buffer bar))) 457 (with-current-buffer bar 458 (call-interactively 'seam-toggle-draft)) 459 (list 460 (seam-test-links-from-html "html/foo.html") 461 (seam-test-list-files)))))) 462 463 (ert-deftest seam-test-unset-draft () 464 "Test that toggling a note from draft to non-draft will export its 465 HTML file and update linking HTML files such that they link to 466 it." 467 (should 468 (equal 469 '(("bar.html") ("html/bar.html" "html/foo.html" "public/bar.org" "public/foo.org")) 470 (seam-test-with-notes () 471 ((foo "foo" "public") 472 (bar "bar" "public" nil t)) 473 (with-current-buffer foo 474 (seam-test-add-contents foo (seam-test-link-to-buffer bar))) 475 (with-current-buffer bar 476 (call-interactively 'seam-toggle-draft)) 477 (list 478 (seam-test-links-from-html "html/foo.html") 479 (seam-test-list-files)))))) 480 481 (ert-deftest seam-test-follow-link-existing () 482 "Test that following a link to an existing note opens that note." 483 (should 484 (equal 485 "bar" 486 (seam-test-with-notes () 487 ((foo "foo") 488 (bar "bar")) 489 (with-current-buffer foo 490 (seam-test-add-contents foo "[[seam:bar]]") 491 (org-previous-link) 492 (org-open-at-point) 493 (buffer-name)))))) 494 495 (ert-deftest seam-test-follow-link-new () 496 "Test that following a link to an nonexistent note creates and 497 opens that note." 498 (should 499 (equal 500 '("bar" ("private/bar.org" "private/foo.org")) 501 (seam-test-with-notes () 502 ((foo "foo")) 503 (with-current-buffer foo 504 (seam-test-add-contents foo "[[seam:bar]]") 505 (goto-char 1) 506 (org-next-link) 507 (org-open-at-point) 508 (unwind-protect 509 (list 510 (buffer-name) 511 (seam-test-list-files)) 512 (kill-buffer))))))) 513 514 (ert-deftest seam-test-follow-link-new-draft () 515 "Test that following a link to an nonexistent draft note creates 516 and opens that note." 517 (should 518 (equal 519 '("-bar" ("private/-bar.org" "private/foo.org")) 520 (seam-test-with-notes () 521 ((foo "foo")) 522 (with-current-buffer foo 523 (seam-test-add-contents foo "[[seam:-bar]]") 524 (goto-char 1) 525 (org-next-link) 526 (org-open-at-point) 527 (unwind-protect 528 (list 529 (buffer-name) 530 (seam-test-list-files)) 531 (kill-buffer))))))) 532 533 (ert-deftest seam-test-escape-title () 534 (should 535 (equal 536 "“quotes” & <symbols>\n“quotes” & <symbols>\n" 537 (seam-test-with-notes ((seam-export-template-string "{{title}}\n{{{raw-title}}}")) 538 ((note "\"quotes\" & <symbols>" "public")) 539 (seam-export--file-string "html/quotes-symbols.html"))))) 540 541 (ert-deftest seam-test-custom-slug () 542 "Test that setting the SEAM_SLUG property saves and exports 543 accordingly." 544 (should 545 (equal 546 '("html/c-vs-cpp.html" "public/c-vs-cpp.org") 547 (seam-test-with-notes () 548 ((note "C vs C++" "public")) 549 (seam-test-add-contents note ":PROPERTIES:\n:SEAM_SLUG: c-vs-cpp\n:END:") 550 (seam-test-list-files))))) 551 552 (ert-deftest seam-test-removing-type-from-export-alist () 553 (should 554 (equal 555 '("public/note.org") 556 (seam-test-with-notes () 557 ((note "Note" "public")) 558 (setq seam-export-alist 559 `((,(file-name-concat seam-test-directory "html") 560 :types ("foo") 561 :root-path "/"))) 562 (seam-export-all-notes) 563 (seam-test-list-files))))) 564 565 (ert-deftest seam-test-template-values () 566 "Test that custom variables can be used in templates, and that 567 existing ones can be overridden." 568 (should 569 (equal 570 "Qux\nhello, world\n" 571 (seam-test-with-notes 572 ((seam-export-template-values '(("title" . "Bar") 573 ("greeting" . "hello, world"))) 574 (seam-export-template-string "{{title}}\n{{greeting}}") 575 (seam-export-alist 576 `((,(file-name-concat seam-test-directory "html") 577 :types ("public") 578 :root-path "/" 579 :template-values (("title" . "Qux")))))) 580 ((foo "Foo" "public")) 581 (seam-export--file-string "html/foo.html"))))) 582 583 (provide 'seam-test) 584 585 ;;; seam-test.el ends here