(ns microtables-frontend.events-test (:require [cljs.test :refer-macros [deftest testing is]] [microtables-frontend.evaluation :as evaluation] [microtables-frontend.utils.data :as data-utils])) ; Tests for the state-transition pipelines that event handlers compose. ; We test the pure functions directly rather than going through re-frame dispatch. ; The event handlers in events.cljs are thin wrappers around these functions. (defn initial-table "Build a fully initialised table-data map (references wired, all formulas evaluated)." [raw-data] (-> raw-data (data-utils/walk-modify-data (fn [_c _r datum] (if (data-utils/formula? (:value datum)) (assoc datum :dirty true) datum))) data-utils/create-all-references evaluation/create-all-back-references evaluation/evaluate-all)) ;; --- Mirrors ::edit-cell-value then ::movement-leave-cell --- (deftest editing-value-then-leaving-triggers-downstream-reevaluation ; Start: A1=3, B1=A1*2 → B1 displays 6 ; Edit A1 to 10, then leave → B1 should display 20 (let [table (initial-table {"A" {1 {:value "3"}} "B" {1 {:value "=A1*2"}}}) updated (-> table (data-utils/change-datum-value "A" 1 "10") (evaluation/reset-references "A" 1) (evaluation/evaluate-from-cell "A" 1))] (is (= 20 (get-in updated ["B" 1 :display]))))) (deftest edit-cell-value-marks-dirty-without-evaluating ; change-datum-value (::edit-cell-value) only updates :value and marks :dirty; ; it does not produce a :display value. (let [table (initial-table {"A" {1 {:value "3"}}}) updated (data-utils/change-datum-value table "A" 1 "=1+2")] (is (= "=1+2" (get-in updated ["A" 1 :value]))) (is (= true (get-in updated ["A" 1 :dirty]))) (is (nil? (get-in updated ["A" 1 :display]))))) (deftest downstream-cells-marked-dirty-on-edit ; When A1 is edited, B1 (which depends on A1) should also be marked dirty. (let [table (initial-table {"A" {1 {:value "5"}} "B" {1 {:value "=A1+1"}}}) updated (data-utils/change-datum-value table "A" 1 "99")] (is (= true (get-in updated ["B" 1 :dirty]))))) ;; --- Mirrors ::movement-leave-cell reference graph update --- (deftest formula-change-removes-old-back-reference ; B1 starts referencing A1. When we change B1 to reference C1, ; A1's :inbound should no longer include B1. (let [table (initial-table {"A" {1 {:value "5"}} "B" {1 {:value "=A1"}}}) updated (-> table (data-utils/change-datum-value "B" 1 "=C1+1") (evaluation/reset-references "B" 1) (evaluation/evaluate-from-cell "B" 1))] (is (not (some #(= % {:col "B" :row 1}) (get-in updated ["A" 1 :inbound])))))) (deftest formula-change-adds-new-back-reference ; Same scenario: after B1 is changed to reference C1, C1's :inbound should include B1. (let [table (initial-table {"A" {1 {:value "5"}} "B" {1 {:value "=A1"}}}) updated (-> table (data-utils/change-datum-value "B" 1 "=C1+1") (evaluation/reset-references "B" 1) (evaluation/evaluate-from-cell "B" 1))] (is (some #(= % {:col "B" :row 1}) (get-in updated ["C" 1 :inbound]))))) ;; --- Deeply nested dependency propagation --- (deftest deep-dependency-chain-reevaluated ; A1 → B1 → C1 → D1; editing A1 should propagate all the way to D1. (let [table (initial-table {"A" {1 {:value "1"}} "B" {1 {:value "=A1+1"}} "C" {1 {:value "=B1+1"}} "D" {1 {:value "=C1+1"}}}) updated (-> table (data-utils/change-datum-value "A" 1 "10") (evaluation/reset-references "A" 1) (evaluation/evaluate-from-cell "A" 1))] (is (= 13 (get-in updated ["D" 1 :display])))))