;;; noise-conversation-test.el --- Tests for noise-conversation -*- lexical-binding: t; -*- (require 'ert) (require 'noise-conversation) (defvar noise-conversation-test--db (expand-file-name "noise-conversation-test.db" temporary-file-directory)) (defmacro noise-conversation-test--with-db (&rest body) "Run BODY with a fresh test database and clean up conversation buffers." `(let ((noise-db-file noise-conversation-test--db)) (unwind-protect (progn (when (file-exists-p noise-db-file) (delete-file noise-db-file)) (noise-db-init) ,@body) (noise-db--close) (when (file-exists-p noise-conversation-test--db) (delete-file noise-conversation-test--db)) (dolist (buf (buffer-list)) (when (string-prefix-p "*signal:" (buffer-name buf)) (kill-buffer buf)))))) (defun noise-conversation-test--seed () "Insert a direct conversation with Sarah and two messages." (noise-db-upsert-contact "+1" :name "Sarah") (noise-db-upsert-conversation "+1" :type "direct" :name "Sarah" :members '("+1")) (noise-db-insert-message "+1" :timestamp 1717930000.0 :source "Sarah" :body "are you coming?") (noise-db-insert-message "+1" :timestamp 1717930100.0 :source "me" :body "yes, leaving now" :receipt-state "delivered")) (ert-deftest noise-conversation-open-renders-history () "Opening a conversation renders messages, a day separator, and the prompt." (noise-conversation-test--with-db (noise-conversation-test--seed) (with-current-buffer (noise-conversation-open "+1") (let ((text (buffer-substring-no-properties (point-min) (point-max)))) (should (string-match-p "Sarah> are you coming\\?" text)) (should (string-match-p "me> yes, leaving now" text)) ;; day separator present (should (string-match-p "─── .* ───" text)) ;; delivered receipt (should (string-match-p "✓✓" text)) ;; compose prompt at the bottom (should (string-match-p "^> " text)))))) (ert-deftest noise-conversation-open-marks-read () "Opening a conversation resets its unread count." (noise-conversation-test--with-db (noise-conversation-test--seed) (noise-db-upsert-conversation "+1" :unread-count 4) (noise-conversation-open "+1") (should (= 0 (gethash "unread_count" (noise-db-get-conversation "+1")))))) (ert-deftest noise-conversation-history-is-read-only () "Typing into the history region should fail; the prompt is editable." (noise-conversation-test--with-db (noise-conversation-test--seed) (with-current-buffer (noise-conversation-open "+1") (goto-char (point-min)) (should-error (insert "x")) (goto-char (point-max)) (insert "hello") (should (string= "hello" (noise-conversation--current-input)))))) (ert-deftest noise-conversation-send-stores-and-clears () "Sending stores the message with the server timestamp and clears input." (noise-conversation-test--with-db (noise-conversation-test--seed) (with-current-buffer (noise-conversation-open "+1") (goto-char (point-max)) (insert "on my way") (let (sent-params) (cl-letf (((symbol-function 'noise-rpc-call-for-account) (lambda (method &rest params) (should (string= "send" method)) (setq sent-params params) (let ((ht (make-hash-table :test 'equal))) (puthash "timestamp" 1717930200123 ht) ht)))) (noise-conversation-send)) (should (equal ["+1"] (plist-get sent-params :recipient))) (should (string= "on my way" (plist-get sent-params :message)))) ;; input cleared, message rendered (should (string= "" (noise-conversation--current-input))) (let ((msgs (noise-db-get-messages "+1"))) (should (= 3 (length msgs))) (let ((last (car (last msgs)))) (should (string= "me" (gethash "source" last))) (should (string= "on my way" (gethash "body" last))) (should (string= "sent" (gethash "receipt_state" last))) (should (= 1717930200.123 (gethash "timestamp" last)))))))) (ert-deftest noise-conversation-send-group-uses-group-id () "Sending in a group conversation uses the groupId parameter." (noise-conversation-test--with-db (noise-db-upsert-conversation "grp1" :type "group" :name "cycling crew") (with-current-buffer (noise-conversation-open "grp1") (goto-char (point-max)) (insert "saturday 7am?") (let (sent-params) (cl-letf (((symbol-function 'noise-rpc-call-for-account) (lambda (_method &rest params) (setq sent-params params) nil))) (noise-conversation-send)) (should (string= "grp1" (plist-get sent-params :groupId))) (should-not (plist-get sent-params :recipient)))))) (ert-deftest noise-conversation-send-empty-does-nothing () "Sending with no input should not call signal-cli." (noise-conversation-test--with-db (noise-conversation-test--seed) (with-current-buffer (noise-conversation-open "+1") (cl-letf (((symbol-function 'noise-rpc-call-for-account) (lambda (&rest _) (error "Should not be called")))) (noise-conversation-send)) (should (= 2 (length (noise-db-get-messages "+1"))))))) (ert-deftest noise-conversation-render-preserves-input () "Re-rendering keeps text already typed at the prompt." (noise-conversation-test--with-db (noise-conversation-test--seed) (with-current-buffer (noise-conversation-open "+1") (goto-char (point-max)) (insert "draft text") (noise-conversation--render) (should (string= "draft text" (noise-conversation--current-input)))))) (ert-deftest noise-conversation-format-attachment () "Attachment messages render the [attachment: ...] hint." (noise-conversation-test--with-db (noise-conversation-test--seed) (noise-db-insert-message "+1" :timestamp 1717930300.0 :source "Sarah" :body "" :has-attachment t :attachment-path "IMG_2041.jpg") (with-current-buffer (noise-conversation-open "+1") (should (string-match-p "\\[attachment: IMG_2041\\.jpg — RET to view\\]" (buffer-string)))))) (ert-deftest noise-conversation-typing-indicator () "Typing STARTED/STOPPED toggles the modeline indicator." (noise-conversation-test--with-db (noise-conversation-test--seed) (with-current-buffer (noise-conversation-open "+1") (noise-conversation-set-typing "+1" "Sarah" "STARTED") (should (string= "Sarah" noise-conversation--typing)) (should (string-match-p "Sarah is typing…" mode-line-format)) (noise-conversation-set-typing "+1" "Sarah" "STOPPED") (should-not noise-conversation--typing))))