From 735ecf082273c0cba2f5c5dd25876e2ccd83b0f7 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 29 Apr 2026 23:59:30 -0500 Subject: test: add gloss-display test suite (red phase) Four test files covering the gloss-display public API and pure helpers. All 23 tests fail at this commit because the implementation is still a stub. Format-candidate gets full N/B/E coverage. Render-entry gets the pure-helper treatment. Pick-definition mocks completing-read at the boundary. Show-entry has a single smoke test. --- tests/test-gloss-display--format-candidate.el | 84 ++++++++++++++++++++++++++ tests/test-gloss-display--pick-definition.el | 85 +++++++++++++++++++++++++++ tests/test-gloss-display--render-entry.el | 64 ++++++++++++++++++++ tests/test-gloss-display--show-entry-smoke.el | 47 +++++++++++++++ 4 files changed, 280 insertions(+) create mode 100644 tests/test-gloss-display--format-candidate.el create mode 100644 tests/test-gloss-display--pick-definition.el create mode 100644 tests/test-gloss-display--render-entry.el create mode 100644 tests/test-gloss-display--show-entry-smoke.el (limited to 'tests') diff --git a/tests/test-gloss-display--format-candidate.el b/tests/test-gloss-display--format-candidate.el new file mode 100644 index 0000000..29e077d --- /dev/null +++ b/tests/test-gloss-display--format-candidate.el @@ -0,0 +1,84 @@ +;;; test-gloss-display--format-candidate.el --- Tests for gloss-display--format-candidate -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; Tests for the pure helper `gloss-display--format-candidate' covering +;; Normal/Boundary/Error cases. The helper takes a definition plist +;; (:source SYM :text STRING) and returns a single-line "[source] text" +;; row suitable for display in `completing-read'. + +;;; Code: + +(require 'ert) +(require 'gloss-display) + +(ert-deftest test-gloss-display-format-candidate-normal-short-text () + "Normal: short text formats as \"[source] text\" with no truncation." + (let ((result (gloss-display--format-candidate + '(:source wiktionary :text "Hello world.")))) + (should (equal result "[wiktionary] Hello world.")))) + +(ert-deftest test-gloss-display-format-candidate-normal-different-source () + "Normal: source symbol is rendered as its name inside brackets." + (let ((result (gloss-display--format-candidate + '(:source dictionary-api :text "Some text.")))) + (should (string-prefix-p "[dictionary-api] " result)))) + +(ert-deftest test-gloss-display-format-candidate-boundary-empty-text () + "Boundary: empty text yields the prefix only." + (let ((result (gloss-display--format-candidate + '(:source wiktionary :text "")))) + (should (equal result "[wiktionary] ")))) + +(ert-deftest test-gloss-display-format-candidate-boundary-single-char-text () + "Boundary: one-character text is preserved verbatim." + (let ((result (gloss-display--format-candidate + '(:source wiktionary :text "a")))) + (should (equal result "[wiktionary] a")))) + +(ert-deftest test-gloss-display-format-candidate-boundary-unicode-text () + "Boundary: unicode characters in text are preserved." + (let ((result (gloss-display--format-candidate + '(:source wiktionary :text "café — naïve résumé")))) + (should (equal result "[wiktionary] café — naïve résumé")))) + +(ert-deftest test-gloss-display-format-candidate-boundary-long-text-truncated () + "Boundary: text longer than the cap is truncated with an ellipsis." + (let* ((long-text (make-string 200 ?x)) + (result (gloss-display--format-candidate + (list :source 'wiktionary :text long-text)))) + (should (<= (length result) gloss-display--candidate-max-length)) + (should (string-suffix-p "..." result)) + (should (string-prefix-p "[wiktionary] " result)))) + +(ert-deftest test-gloss-display-format-candidate-boundary-text-at-cap-not-truncated () + "Boundary: text that fits exactly at the cap is not truncated." + (let* ((prefix-len (length "[wiktionary] ")) + (text (make-string (- gloss-display--candidate-max-length prefix-len) ?y)) + (result (gloss-display--format-candidate + (list :source 'wiktionary :text text)))) + (should (= (length result) gloss-display--candidate-max-length)) + (should-not (string-suffix-p "..." result)))) + +(ert-deftest test-gloss-display-format-candidate-boundary-missing-source () + "Boundary: missing :source falls back to the literal \"unknown\" tag." + (let ((result (gloss-display--format-candidate + '(:text "Some text.")))) + (should (string-prefix-p "[unknown] " result)) + (should (string-suffix-p "Some text." result)))) + +(ert-deftest test-gloss-display-format-candidate-boundary-newlines-collapsed () + "Boundary: embedded newlines are collapsed to spaces (single-line row)." + (let ((result (gloss-display--format-candidate + '(:source wiktionary :text "First line.\nSecond line.")))) + (should-not (string-match-p "\n" result)) + (should (string-match-p "First line. Second line." result)))) + +(ert-deftest test-gloss-display-format-candidate-error-non-list-input () + "Error: passing a non-list raises `wrong-type-argument'." + (should-error (gloss-display--format-candidate "not a plist") + :type 'wrong-type-argument)) + +(provide 'test-gloss-display--format-candidate) +;;; test-gloss-display--format-candidate.el ends here diff --git a/tests/test-gloss-display--pick-definition.el b/tests/test-gloss-display--pick-definition.el new file mode 100644 index 0000000..0c11fe1 --- /dev/null +++ b/tests/test-gloss-display--pick-definition.el @@ -0,0 +1,85 @@ +;;; test-gloss-display--pick-definition.el --- Tests for gloss-display-pick-definition -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; Tests for `gloss-display-pick-definition' covering Normal/Boundary/Error +;; cases. The picker mocks `completing-read' at the boundary; the function +;; under test is responsible for building the alist of formatted-string -> +;; plist and mapping the user's choice back to the chosen plist. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'gloss-display) + +(defmacro gloss-test--with-completing-read (return-value &rest body) + "Run BODY with `completing-read' mocked to return RETURN-VALUE. +If RETURN-VALUE is the symbol `quit', simulate the user pressing C-g +by signalling `quit' inside the mock." + (declare (indent 1) (debug t)) + `(cl-letf (((symbol-function 'completing-read) + (lambda (&rest _args) + ,(if (eq return-value 'quit) + '(signal 'quit nil) + return-value)))) + ,@body)) + +(ert-deftest test-gloss-display-pick-definition-normal-pick-first-of-three () + "Normal: choosing the first row returns the first definition plist." + (let* ((def1 '(:source wiktionary :text "First sense.")) + (def2 '(:source wiktionary :text "Second sense.")) + (def3 '(:source wiktionary :text "Third sense.")) + (defs (list def1 def2 def3)) + (chosen-row (gloss-display--format-candidate def1))) + (gloss-test--with-completing-read chosen-row + (should (equal (gloss-display-pick-definition "term" defs) def1))))) + +(ert-deftest test-gloss-display-pick-definition-normal-pick-middle-of-three () + "Normal: choosing the middle row returns the middle definition plist." + (let* ((def1 '(:source wiktionary :text "First sense.")) + (def2 '(:source wiktionary :text "Second sense.")) + (def3 '(:source wiktionary :text "Third sense.")) + (defs (list def1 def2 def3)) + (chosen-row (gloss-display--format-candidate def2))) + (gloss-test--with-completing-read chosen-row + (should (equal (gloss-display-pick-definition "term" defs) def2))))) + +(ert-deftest test-gloss-display-pick-definition-boundary-single-definition () + "Boundary: a single-definition list still returns that plist." + (let* ((def '(:source wiktionary :text "Only sense.")) + (chosen-row (gloss-display--format-candidate def))) + (gloss-test--with-completing-read chosen-row + (should (equal (gloss-display-pick-definition "term" (list def)) + def))))) + +(ert-deftest test-gloss-display-pick-definition-boundary-empty-list () + "Boundary: an empty definition list returns nil without prompting." + (let ((completing-read-called nil)) + (cl-letf (((symbol-function 'completing-read) + (lambda (&rest _args) + (setq completing-read-called t) ""))) + (should-not (gloss-display-pick-definition "term" nil)) + (should-not completing-read-called)))) + +(ert-deftest test-gloss-display-pick-definition-error-quit-returns-nil () + "Error: C-g during the picker (quit signal) returns nil, not propagate." + (let ((def '(:source wiktionary :text "Only sense."))) + (gloss-test--with-completing-read quit + (should-not (gloss-display-pick-definition "term" (list def)))))) + +(ert-deftest test-gloss-display-pick-definition-prompt-mentions-term () + "Normal: the completing-read prompt names the term being disambiguated." + (let* ((captured-prompt nil) + (def '(:source wiktionary :text "Only sense.")) + (chosen-row (gloss-display--format-candidate def))) + (cl-letf (((symbol-function 'completing-read) + (lambda (prompt &rest _args) + (setq captured-prompt prompt) + chosen-row))) + (gloss-display-pick-definition "anaphora" (list def)) + (should (string-match-p "anaphora" captured-prompt))))) + +(provide 'test-gloss-display--pick-definition) +;;; test-gloss-display--pick-definition.el ends here diff --git a/tests/test-gloss-display--render-entry.el b/tests/test-gloss-display--render-entry.el new file mode 100644 index 0000000..5656660 --- /dev/null +++ b/tests/test-gloss-display--render-entry.el @@ -0,0 +1,64 @@ +;;; test-gloss-display--render-entry.el --- Tests for gloss-display--render-entry -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; Tests for the pure helper `gloss-display--render-entry' covering +;; Normal/Boundary/Error cases. The helper takes a TERM and BODY and +;; returns the formatted string that `gloss-display-show-entry' inserts +;; into the side buffer. + +;;; Code: + +(require 'ert) +(require 'gloss-display) + +(ert-deftest test-gloss-display-render-entry-normal-term-and-body () + "Normal: render produces the term, an underline, a blank, and the body." + (let ((result (gloss-display--render-entry + "anaphora" + "Reference to something earlier in the discourse."))) + (should (string-match-p "^anaphora\n" result)) + (should (string-match-p "^=+\n" (substring result (length "anaphora\n")))) + (should (string-match-p "Reference to something earlier" result)))) + +(ert-deftest test-gloss-display-render-entry-normal-underline-matches-term-length () + "Normal: the underline length matches the term length." + (let* ((term "abc") + (result (gloss-display--render-entry term "Body.")) + (lines (split-string result "\n"))) + (should (equal (nth 0 lines) term)) + (should (= (length (nth 1 lines)) (length term))) + (should (string-match-p "^=+$" (nth 1 lines))))) + +(ert-deftest test-gloss-display-render-entry-boundary-multi-line-body () + "Boundary: a multi-line body is preserved verbatim." + (let ((result (gloss-display--render-entry + "term" + "First paragraph.\n\nSecond paragraph."))) + (should (string-match-p "First paragraph\\." result)) + (should (string-match-p "Second paragraph\\." result)) + (should (string-match-p "First paragraph\\.\n\nSecond paragraph\\." result)))) + +(ert-deftest test-gloss-display-render-entry-boundary-empty-body () + "Boundary: an empty body still renders the term and underline." + (let ((result (gloss-display--render-entry "lonely" ""))) + (should (string-match-p "^lonely\n=+\n" result)))) + +(ert-deftest test-gloss-display-render-entry-boundary-unicode-term () + "Boundary: unicode characters in the term are preserved." + (let* ((term "café") + (result (gloss-display--render-entry term "A coffeehouse."))) + (should (string-match-p (regexp-quote term) result)) + (should (string-match-p "A coffeehouse\\." result)))) + +(ert-deftest test-gloss-display-render-entry-boundary-very-long-term () + "Boundary: a long term gets a matching long underline." + (let* ((term (make-string 60 ?z)) + (result (gloss-display--render-entry term "Body.")) + (lines (split-string result "\n"))) + (should (equal (nth 0 lines) term)) + (should (= (length (nth 1 lines)) 60)))) + +(provide 'test-gloss-display--render-entry) +;;; test-gloss-display--render-entry.el ends here diff --git a/tests/test-gloss-display--show-entry-smoke.el b/tests/test-gloss-display--show-entry-smoke.el new file mode 100644 index 0000000..07d7a97 --- /dev/null +++ b/tests/test-gloss-display--show-entry-smoke.el @@ -0,0 +1,47 @@ +;;; test-gloss-display--show-entry-smoke.el --- Smoke test for gloss-display-show-entry -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; Smoke test for `gloss-display-show-entry'. Exercises the side-buffer +;; setup end to end: buffer creation, content rendering, mode activation, +;; and the read-only invariant. Per the design and project testing rules, +;; framework primitives like `display-buffer' and major-mode mechanics are +;; not re-tested. + +;;; Code: + +(require 'ert) +(require 'gloss-display) + +(defmacro gloss-test--with-display-buffer-mocked (&rest body) + "Run BODY with `display-buffer' replaced by a no-op. +Avoids opening a real side window during batch tests, while still letting +`gloss-display-show-entry' populate and configure the entry buffer." + (declare (indent 0) (debug t)) + `(cl-letf (((symbol-function 'display-buffer) + (lambda (buffer &rest _args) (get-buffer buffer)))) + ,@body)) + +(ert-deftest test-gloss-display-show-entry-creates-buffer-with-mode-and-content () + "Smoke: show-entry creates a named buffer in `gloss-mode' with the entry." + (gloss-test--with-display-buffer-mocked + (unwind-protect + (let ((buf (gloss-display-show-entry + "anaphora" + "Reference to something earlier in the discourse."))) + (should (buffer-live-p buf)) + (with-current-buffer buf + (should (derived-mode-p 'gloss-mode)) + (should buffer-read-only) + (let ((contents (buffer-substring-no-properties + (point-min) (point-max)))) + (should (string-match-p "anaphora" contents)) + (should (string-match-p "Reference to something earlier" + contents))))) + (when-let ((buf (get-buffer "*gloss: anaphora*"))) + (let ((kill-buffer-query-functions nil)) + (kill-buffer buf)))))) + +(provide 'test-gloss-display--show-entry-smoke) +;;; test-gloss-display--show-entry-smoke.el ends here -- cgit v1.2.3