penpot/common/test/common_tests/files/tokens_test.cljc
Andrés Moya feb73f0fd8
✨ Handle tokens libraries when exporting .penpot files (#12077)
* ✨ Handle tokens libraries when exporting .penpot files

Exporting a file that takes its tokens from an external library left
the :tokens-source of the file pointing at a library that is not in the
export, and the tokens lib of that library was not exported.

With :detach-libraries, unset the :tokens-source and the tokens status
in detach-external-references. The file is exported without any link to
the library, and the shapes keep their applied tokens.

With :merge-libraries, add cfo/absorb-tokens-lib, which copies the tokens
lib of a library into a file and unsets its :tokens-source, so the file
becomes its own tokens source. The tokens status of the file is kept; if
it has none, the one of the library is copied. If the library has no
tokens lib, only the source is unset. embed-assets calls it for the
library that is the tokens source of the file, and ignores the other
linked libraries.

Add unit tests for absorb-tokens-lib, and binfile tests that export a
file with tokens from an external library with both modes, checking the
exported files, the tokens lib, the tokens source and the applied
tokens.

Also reformat the export-type docstring in binfile v3 and remove a
leftover debug prn in the export dialog.

AI-assisted-by: claude-sonnet-5-5

* 🐛 Preserve active token sets when importing linked libraries

Cache each decoded tokens library for the duration of an import so files
and their tokens source resolve active sets against the same generated IDs.
Add a round-trip regression test for exports that include linked libraries.

* 🐛 Preserve tokens when deleting or unpublishing libraries

Absorb the tokens library into files that use it as their tokens source.
Keep referencing file IDs in the deletion task before removing relations.

Embed tokens from deleted, unlinked sources during export, and clear stale
source and status data when the library no longer exists. Add regression
tests for deletion, unpublishing, and exports with unavailable libraries.

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-15 16:53:30 +02:00

1712 lines
98 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns common-tests.files.tokens-test
(:require
[app.common.files.tokens :as cfo]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.tokens :as tht]
[app.common.types.file :as ctf]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos]
[app.common.uuid :as uuid]
[clojure.test :as t]))
;; Helper functions
(t/deftest test-token-circular-reference?
(t/testing "returns nil when token has no references"
(let [tokens {"spacing" {:name "spacing" :value "16px"}}]
(t/is (nil? (cfo/token-circular-reference? tokens "spacing")))))
(t/testing "returns nil for a simple non-circular reference chain"
(let [tokens {"base" {:name "base" :value "10"}
"large" {:name "large" :value "{base}"}}]
(t/is (nil? (cfo/token-circular-reference? tokens "large")))))
(t/testing "detects direct self-reference"
(let [tokens {"self" {:name "self" :value "{self}"}}]
(t/is (= "self" (cfo/token-circular-reference? tokens "self")))))
(t/testing "detects indirect circular reference"
(let [tokens {"a" {:name "a" :value "{b}"}
"b" {:name "b" :value "{c}"}
"c" {:name "c" :value "{a}"}}]
(t/is (some? (cfo/token-circular-reference? tokens "a")))))
(t/testing "returns nil for diamond dependency (no cycle)"
(let [tokens {"base" {:name "base" :value "10"}
"left" {:name "left" :value "{base}"}
"right" {:name "right" :value "{base}"}
"combine" {:name "combine" :value "{left} {right}"}}]
(t/is (nil? (cfo/token-circular-reference? tokens "combine")))))
(t/testing "returns nil when token name does not exist in tokens map"
(let [tokens {"a" {:name "a" :value "10"}}]
(t/is (nil? (cfo/token-circular-reference? tokens "nonexistent")))))
(t/testing "detects circular reference in map-typed values (typography composite)"
(let [tokens {"family" {:name "family" :value "Arial"}
"size" {:name "size" :value "16px"}
"typog" {:name "typog" :value {:font-family {:reference "{family}"}
:font-size {:reference "{size}"}}}
"cycle" {:name "cycle" :value {:font-family {:reference "{typog}"}}}}]
;; typog references family and size (no cycle)
(t/is (nil? (cfo/token-circular-reference? tokens "typog"))))))
(t/deftest test-attributes-map
(t/testing "creates map from attributes to token name"
(t/is (= {:fill "primary" :stroke "primary"}
(cfo/attributes-map [:fill :stroke] {:name "primary"}))))
(t/testing "returns empty map for empty attributes"
(t/is (= {} (cfo/attributes-map [] {:name "primary"}))))
(t/testing "maps each attribute independently"
(t/is (= {:x "t" :y "t" :z "t"}
(cfo/attributes-map [:x :y :z] {:name "t"})))))
(t/deftest test-remove-attributes-for-token
(t/testing "removes entries matching both attribute set and token name"
(t/is (= {:stroke "other"}
(cfo/remove-attributes-for-token
[:fill :stroke] "primary"
{:fill "primary" :stroke "other"}))))
(t/testing "keeps entries with matching attribute but different token name"
(t/is (= {:fill "secondary" :stroke "other"}
(cfo/remove-attributes-for-token
[:fill :stroke] "primary"
{:fill "secondary" :stroke "other"}))))
(t/testing "keeps entries with matching token name but non-matching attribute"
(t/is (= {:opacity "primary"}
(cfo/remove-attributes-for-token
[:fill :stroke] "primary"
{:fill "primary" :opacity "primary"}))))
(t/testing "returns empty map when all entries match"
(t/is (= {}
(cfo/remove-attributes-for-token
[:fill] "primary"
{:fill "primary"}))))
(t/testing "returns empty map for empty applied-tokens"
(t/is (= {}
(cfo/remove-attributes-for-token [:fill] "primary" {})))))
(t/deftest test-color-token?
(t/testing "returns true for color type"
(t/is (true? (cfo/color-token? {:type :color}))))
(t/testing "returns false for non-color types"
(t/is (false? (cfo/color-token? {:type :spacing})))
(t/is (false? (cfo/color-token? {:type :border-radius})))
(t/is (false? (cfo/color-token? {})))))
(t/deftest test-is-reference?
(t/testing "returns truthy when value contains opening brace"
(t/is (cfo/is-reference? {:value "{foo}"}))
(t/is (cfo/is-reference? {:value "calc({bar} + 1)"})))
(t/testing "returns falsy when value has no brace"
(t/is (not (cfo/is-reference? {:value "16px"})))
(t/is (not (cfo/is-reference? {:value ""})))))
(t/deftest test-parse-token-value
(t/testing "parses double from a token value"
(t/is (= {:value 100.1 :unit nil} (cfo/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cfo/parse-token-value "-9"))))
(t/testing "trims white-space"
(t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
(t/testing "parses unit: px"
(t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
(t/testing "parses unit: %"
(t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
(t/testing "parses unit: px")
(t/testing "returns nil for any invalid characters"
(t/is (nil? (cfo/parse-token-value " -1.3a "))))
(t/testing "doesnt accept invalid double"
(t/is (nil? (cfo/parse-token-value ".3")))))
(t/deftest convert-dtcg-token-test
(t/testing "keeps string scalar values untouched"
(t/is (= {:name "spacing.16" :type :spacing :value "16"}
(cfo/convert-dtcg-token {"name" "spacing.16" "type" "spacing" "value" "16"})))
(t/is (= {:name "spacing.16" :type :spacing :value "16px"}
(cfo/convert-dtcg-token {"name" "spacing.16" "type" "spacing" "value" "16px"}))))
(t/testing "coerces numeric scalar values to strings"
(t/is (= {:name "spacing.16" :type :spacing :value "16"}
(cfo/convert-dtcg-token {"name" "spacing.16" "type" "spacing" "value" 16})))
(t/is (= {:name "radius" :type :border-radius :value "4"}
(cfo/convert-dtcg-token {"name" "radius" "type" "borderRadius" "value" 4})))))
(t/deftest token-applied-test
(t/testing "matches passed token with `:token-attributes`"
(t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match empty token"
(t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "does't match passed token `:id`"
(t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match passed `:token-attributes`"
(t/is (nil? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/deftest shapes-ids-by-applied-attributes
(t/testing "Returns set of matched attributes that fit the applied token"
(let [attributes #{:x :y :z}
shape-applied-x {:id "shape-applied-x"
:applied-tokens {:x "1"}}
shape-applied-y {:id "shape-applied-y"
:applied-tokens {:y "1"}}
shape-applied-x-y {:id "shape-applied-x-y"
:applied-tokens {:x "1" :y "1"}}
shape-applied-none {:id "shape-applied-none"
:applied-tokens {}}
shape-applied-all {:id "shape-applied-all"
:applied-tokens {:x "1" :y "1" :z "1"}}
shape-ids (fn [& xs] (into #{} (map :id xs)))
shapes [shape-applied-x
shape-applied-y
shape-applied-x-y
shape-applied-all
shape-applied-none]
expected (cfo/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
(t/is (= (:x expected) (shape-ids shape-applied-x
shape-applied-x-y
shape-applied-all)))
(t/is (= (:y expected) (shape-ids shape-applied-y
shape-applied-x-y
shape-applied-all)))
(t/is (= (:z expected) (shape-ids shape-applied-all)))
(t/is (true? (cfo/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
(t/is (false? (cfo/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
(shape-ids shape-applied-x
shape-applied-x-y
shape-applied-all))))
(t/deftest tokens-applied-test
(t/testing "is true when single shape matches the token and attributes"
(t/is (true? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "b"}}]
#{:x}))))
(t/testing "is false when no shape matches the token or attributes"
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
{:applied-tokens {:x "b"}}]
#{:x})))
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "a"}}]
#{:y})))))
;; Tokens lib in file data
(t/deftest test-ensure-tokens-lib
(t/testing "ensure-tokens-lib should add a tokens-lib and tokens-status to the file data if they are missing"
(let [file (thf/sample-file :file1)
file-data (ctf/file-data file)
file-data' (cfo/ensure-tokens-lib file-data)]
(t/is (contains? file-data' :tokens-lib))
(t/is (ctob/tokens-lib? (:tokens-lib file-data')))
(t/is (contains? file-data' :tokens-status))
(t/is (ctos/tokens-status? (:tokens-status file-data')))))
(t/testing "ensure-tokens-lib should add a tokens-lib to the file data if it's missing"
(let [tokens-status (ctos/make-tokens-status)
file (-> (thf/sample-file :file1)
(assoc-in [:data :tokens-status] tokens-status))
file-data (ctf/file-data file)
file-data' (cfo/ensure-tokens-lib file-data)]
(t/is (contains? file-data' :tokens-lib))
(t/is (ctob/tokens-lib? (:tokens-lib file-data')))
(t/is (= tokens-status (:tokens-status file-data')))))
(t/testing "ensure-tokens-lib should add a tokens-status to the file data if it's missing"
(let [tokens-lib (ctob/make-tokens-lib)
file (-> (thf/sample-file :file1)
(assoc-in [:data :tokens-lib] tokens-lib))
file-data (ctf/file-data file)
file-data' (cfo/ensure-tokens-lib file-data)]
(t/is (= tokens-lib (:tokens-lib file-data')))
(t/is (contains? file-data' :tokens-status))
(t/is (ctos/tokens-status? (:tokens-status file-data')))))
(t/testing "ensure-tokens-lib should not add a tokens-lib if there is a tokens-source"
(let [source-id (uuid/next)
tokens-status (ctos/make-tokens-status)
file (-> (thf/sample-file :file1)
(assoc-in [:data :tokens-source] source-id)
(assoc-in [:data :tokens-status] tokens-status))
file-data (ctf/file-data file)
file-data' (cfo/ensure-tokens-lib file-data)]
(t/is (not (contains? file-data' :tokens-lib)))
(t/is (= source-id (:tokens-source file-data')))
(t/is (= tokens-status (:tokens-status file-data')))))
(t/testing "ensure-tokens-lib should not add a tokens-lib if there is a tokens-source, but should add a tokens-status if it's missing"
(let [source-id (uuid/next)
file (-> (thf/sample-file :file1)
(assoc-in [:data :tokens-source] source-id))
file-data (ctf/file-data file)
file-data' (cfo/ensure-tokens-lib file-data)]
(t/is (not (contains? file-data' :tokens-lib)))
(t/is (= source-id (:tokens-source file-data')))
(t/is (contains? file-data' :tokens-status))
(t/is (ctos/tokens-status? (:tokens-status file-data')))))
(t/testing "ensure-tokens-lib should add a tokens-lib if the tokens-source is the file itself"
(let [file (thf/sample-file :file1)
file-data (-> (ctf/file-data file)
(assoc :tokens-source (:id file)))
file-data' (cfo/ensure-tokens-lib file-data)]
(t/is (ctob/tokens-lib? (:tokens-lib file-data')))
(t/is (ctos/tokens-status? (:tokens-status file-data'))))))
(t/deftest test-get-tokens-lib
(t/testing "returns tokens-lib from file data"
(let [tokens-lib (ctob/make-tokens-lib)
file-data {:tokens-lib tokens-lib}]
(t/is (= tokens-lib (cfo/get-tokens-lib file-data)))))
(t/testing "returns nil when no tokens-lib"
(t/is (nil? (cfo/get-tokens-lib {})))))
(t/deftest test-get-tokens-source
(t/testing "returns tokens-source from file data"
(let [source-id (thi/new-id! :tokens-source)
file (-> (thf/sample-file :file1)
(assoc-in [:data :tokens-source] source-id))]
(t/is (= source-id (cfo/get-tokens-source (:data file))))))
(t/testing "returns nil when no tokens-source"
(let [file (thf/sample-file :file1)]
(t/is (nil? (cfo/get-tokens-source (:data file)))))))
(t/deftest test-get-effective-tokens-source
(t/testing "returns tokens-source from file data"
(let [source-id (thi/new-id! :tokens-source)
file (-> (thf/sample-file :file1)
(assoc-in [:data :tokens-source] source-id))]
(t/is (= source-id (cfo/get-effective-tokens-source (:data file))))))
(t/testing "returns file id when no tokens-source"
(let [file (thf/sample-file :file1)]
(t/is (= (:id file) (cfo/get-effective-tokens-source file))))))
(t/deftest test-set-tokens-source
(t/testing "sets tokens-source on file data"
(let [source-id (thi/new-id! :tokens-source)
file (thf/sample-file :file1)
file-data (:data file)
file-data' (cfo/set-tokens-source file-data source-id)]
(t/is (= source-id (:tokens-source file-data')))))
(t/testing "removes tokens-source when set to the file itself"
(let [source-id (thi/new-id! :tokens-source)
file (thf/sample-file :file1)
file-data (-> (ctf/file-data file)
(assoc :tokens-source source-id))
file-data' (cfo/set-tokens-source file-data (:id file))]
(t/is (not (contains? file-data' :tokens-source)))
(t/is (= (:id file) (cfo/get-effective-tokens-source file-data'))))))
(t/deftest test-effective-tokens-source?
(t/testing "no tokens, no tokens-source, ask for file id -> true"
(let [file (thf/sample-file :file1)
file-data (:data file)]
(t/is (true? (cfo/effective-tokens-source? file-data (:id file-data))))))
(t/testing "empty tokens-lib, no tokens-source, ask for file id -> true"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (:data file)]
(t/is (true? (cfo/effective-tokens-source? file-data (:id file-data))))))
(t/testing "tokens, no tokens-source, ask for file id -> true"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (:data file)]
(t/is (true? (cfo/effective-tokens-source? file-data (:id file-data))))))
(t/testing "no tokens, no tokens source, ask for other id -> false"
(let [file (thf/sample-file :file1)
file-data (:data file)]
(t/is (false? (cfo/effective-tokens-source? file-data (uuid/next))))))
(t/testing "empty tokens-lib, no tokens source, ask for other id -> false"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (:data file)]
(t/is (false? (cfo/effective-tokens-source? file-data (uuid/next))))))
(t/testing "tokens, no tokens source, ask for other id -> false"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (:data file)]
(t/is (false? (cfo/effective-tokens-source? file-data (uuid/next))))))
;; ---
(t/testing "no tokens, tokens source = file id, ask for file id -> true" ;; This case should not occur
(let [file (thf/sample-file :file1)
file-data (cfo/set-tokens-source (:data file) (:id (:data file)))]
(t/is (true? (cfo/effective-tokens-source? file-data (:id file-data))))))
(t/testing "empty tokens-lib, tokens source = file id, ask for file id -> true" ;; This case should not occur
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (cfo/set-tokens-source (:data file) (:id (:data file)))]
(t/is (true? (cfo/effective-tokens-source? file-data (:id file-data))))))
(t/testing "tokens, tokens source = file id, ask for file id -> true"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (cfo/set-tokens-source (:data file) (:id (:data file)))]
(t/is (true? (cfo/effective-tokens-source? file-data (:id file-data))))))
(t/testing "no tokens, tokens source = file id, ask for other id -> false" ;; This case should not occur
(let [file (thf/sample-file :file1)
file-data (cfo/set-tokens-source (:data file) (:id (:data file)))]
(t/is (false? (cfo/effective-tokens-source? file-data (uuid/next))))))
(t/testing "empty tokens-lib, tokens source = file id, ask for other id -> false" ;; This case should not occur
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (cfo/set-tokens-source (:data file) (:id (:data file)))]
(t/is (false? (cfo/effective-tokens-source? file-data (uuid/next))))))
(t/testing "tokens, tokens source = file id, ask for other id -> false"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (cfo/set-tokens-source (:data file) (:id (:data file)))]
(t/is (false? (cfo/effective-tokens-source? file-data (uuid/next))))))
;; ---
(t/testing "no tokens, tokens source = other id, ask for file id -> false"
(let [source-id (thi/new-id! :tokens-source)
file (thf/sample-file :file1)
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (false? (cfo/effective-tokens-source? file-data (:id file-data))))))
(t/testing "empty tokens-lib, tokens source = other id, ask for file id -> false"
(let [source-id (thi/new-id! :tokens-source)
file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (false? (cfo/effective-tokens-source? file-data (:id file-data))))))
(t/testing "tokens, tokens source = other id, ask for file id -> false"
(let [source-id (thi/new-id! :tokens-source)
file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (false? (cfo/effective-tokens-source? file-data (:id file-data))))))
(t/testing "no tokens, tokens source = other id, ask for this same id -> true"
(let [source-id (thi/new-id! :tokens-source)
file (thf/sample-file :file1)
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (true? (cfo/effective-tokens-source? file-data source-id)))))
(t/testing "empty tokens-lib, tokens source = other id, ask for this same id -> true"
(let [source-id (thi/new-id! :tokens-source)
file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (true? (cfo/effective-tokens-source? file-data source-id)))))
(t/testing "tokens, tokens source = other id, ask for this same id -> true"
(let [source-id (thi/new-id! :tokens-source)
file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (true? (cfo/effective-tokens-source? file-data source-id)))))
(t/testing "no tokens, tokens source = other id, ask for a different id -> false"
(let [source-id (thi/new-id! :tokens-source)
file (thf/sample-file :file1)
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (false? (cfo/effective-tokens-source? file-data (uuid/next))))))
(t/testing "empty tokens-lib, tokens source = other id, ask for a different id -> false"
(let [source-id (thi/new-id! :tokens-source)
file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (false? (cfo/effective-tokens-source? file-data (uuid/next))))))
(t/testing "tokens, tokens source = other id, ask for a different id -> false"
(let [source-id (thi/new-id! :tokens-source)
file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (false? (cfo/effective-tokens-source? file-data (uuid/next)))))))
(t/deftest test-has-own-tokens?
(t/testing "no tokens-lib -> false"
(let [file (thf/sample-file :file1)
file-data (:data file)]
(t/is (false? (cfo/has-own-tokens? file-data)))))
(t/testing "empty tokens-lib -> false"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (:data file)]
(t/is (false? (cfo/has-own-tokens? file-data)))))
(t/testing "tokens lib with data -> true"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (:data file)]
(t/is (true? (cfo/has-own-tokens? file-data))))))
(t/deftest test-tokens-provider?
(t/testing "returns false if it has no tokens"
(let [file (thf/sample-file :file1)
file-data (:data file)]
(t/is (false? (cfo/tokens-provider? file-data)))))
(t/testing "returns false if it has an empty tokens-lib and no tokens-source"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (:data file)]
(t/is (false? (cfo/tokens-provider? file-data)))))
(t/testing "returns false if it has an empty tokens lib and tokens-source is its own id"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file-data (cfo/set-tokens-source (:data file) (:id file))]
(t/is (false? (cfo/tokens-provider? file-data)))))
(t/testing "returns true if it has tokens and no tokens-source"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (:data file)]
(t/is (true? (cfo/tokens-provider? file-data)))))
(t/testing "returns true if it has tokens and tokens-source is its own id"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (cfo/set-tokens-source (:data file) (:id file))]
(t/is (true? (cfo/tokens-provider? file-data)))))
(t/testing "returns false if it has tokens and tokens-source is other id"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
#(-> %
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px")))))
file-data (cfo/set-tokens-source (:data file) (uuid/next))]
(t/is (false? (cfo/tokens-provider? file-data))))))
(t/deftest test-editable-tokens?
(t/testing "returns true when file-data has no tokens-source"
(let [file (thf/sample-file :file1)
file-data (:data file)]
(t/is (true? (cfo/editable-tokens? file-data)))))
(t/testing "returns true when tokens-source is the file's own id"
(let [file (thf/sample-file :file1)
file-data (cfo/set-tokens-source (:data file) (:id (:data file)))]
(t/is (true? (cfo/editable-tokens? file-data)))))
(t/testing "returns false when tokens-source is a different uuid"
(let [source-id (thi/new-id! :tokens-source)
file (thf/sample-file :file1)
file-data (cfo/set-tokens-source (:data file) source-id)]
(t/is (false? (cfo/editable-tokens? file-data))))))
(t/deftest test-get-tokens-status
(t/testing "returns tokens-status from file data"
(let [tokens-status (ctos/make-tokens-status)
file-data {:tokens-status tokens-status}]
(t/is (= tokens-status (cfo/get-tokens-status file-data)))))
(t/testing "returns nil when no tokens-status"
(t/is (nil? (cfo/get-tokens-status {})))))
(t/deftest test-update-tokens-lib
(t/testing "update when there is no tokens-lib has no effect"
(let [file (thf/sample-file :file1)
file' (cfo/update-tokens-lib file #(t/is false "This should not be called"))]
(t/is (= file file'))))
(t/testing "update a tokens-lib applies the changes correctly"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib))
file' (ctf/update-file-data file
#(cfo/update-tokens-lib
%
ctob/add-theme
(ctob/make-token-theme :id (thi/new-id! :theme1)
:name "theme 1")))
tokens-lib' (tht/get-tokens-lib file')]
(t/is (= 2 (ctob/theme-count tokens-lib'))) ;; Count the hidden theme
(t/is (ctob/token-theme? (ctob/get-theme tokens-lib' (thi/id :theme1)))))))
(t/deftest test-update-tokens-status
(t/testing "update when there is no tokens-status has no effect"
(let [file (thf/sample-file :file1)
file' (cfo/update-tokens-status file #(t/is false "This should not be called"))]
(t/is (= file file'))))
(t/testing "update a tokens-status applies the changes correctly"
(let [file (-> (thf/sample-file :file1)
(tht/add-tokens-lib)
(tht/update-tokens-lib
(fn [tokens-lib]
(-> tokens-lib
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme1) :name "theme"))))))
file' (ctf/update-file-data file
#(cfo/update-tokens-status
%
ctos/set-tokens-status
#{(thi/id :theme1)}
#{}))
tokens-status' (tht/get-tokens-status file')]
(t/is (= 1 (count (ctos/get-active-theme-ids tokens-status'))))
(t/is (ctos/theme-active? tokens-status' (thi/id :theme1)))
(t/is (= 0 (count (ctos/get-active-set-ids tokens-status')))))))
;; Tokens status with tokens lib
;; This is a private function but it deserves specific tests for all use cases
(def calculate-active-sets #'app.common.files.tokens/calculate-active-sets)
(t/deftest test-calculate-active-sets
(t/testing "returns union of sets from all active themes"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b"))
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-c) :name "set-c"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a" "set-b"}))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2)
:name "theme-2"
:group ""
:sets #{"set-c"})))]
(t/is (= #{(thi/id :set-a) (thi/id :set-b) (thi/id :set-c)}
(calculate-active-sets #{(thi/id :theme-1) (thi/id :theme-2)} tokens-lib)))))
(t/testing "deduplicates sets shared by multiple active themes"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a" "set-b"}))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2)
:name "theme-2"
:group ""
:sets #{"set-b"})))]
(t/is (= #{(thi/id :set-a) (thi/id :set-b)}
(calculate-active-sets #{(thi/id :theme-1) (thi/id :theme-2)} tokens-lib)))))
(t/testing "returns empty set when active-theme-ids is empty"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"})))]
(t/is (= #{}
(calculate-active-sets #{} tokens-lib)))))
(t/testing "ignores active themes that do not exist in the lib"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"})))]
(t/is (= #{(thi/id :set-a)}
(calculate-active-sets #{(thi/id :theme-1) (thi/new-id! :nonexistent)} tokens-lib)))))
(t/testing "ignores set names that do not exist in the lib"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a" "nonexistent-set"})))]
(t/is (contains? (calculate-active-sets #{(thi/id :theme-1)} tokens-lib) (thi/id :set-a)))
(t/is (= 2 (count (calculate-active-sets #{(thi/id :theme-1)} tokens-lib))))))
(t/testing "returns empty set when themes have no sets"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{})))]
(t/is (= #{}
(calculate-active-sets #{(thi/id :theme-1)} tokens-lib)))))
(t/testing "returns empty set when there are no themes"
(let [tokens-lib (-> (ctob/make-tokens-lib))]
(t/is (= #{}
(calculate-active-sets #{} tokens-lib))))))
(t/deftest test-get-active-themes
(t/testing "returns only themes that are active in the status"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) :name "theme-1" :group ""))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2) :name "theme-2" :group ""))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-3) :name "theme-3" :group "")))
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1) (thi/id :theme-3)}
:active-set-ids #{})]
(t/is (= 2 (count (cfo/get-active-themes tokens-status tokens-lib))))
(let [active-ids (into #{} (map ctob/get-id) (cfo/get-active-themes tokens-status tokens-lib))]
(t/is (contains? active-ids (thi/id :theme-1)))
(t/is (contains? active-ids (thi/id :theme-3)))
(t/is (not (contains? active-ids (thi/id :theme-2)))))))
(t/testing "returns empty sequence when no themes are active"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) :name "theme-1" :group "")))
tokens-status (ctos/make-tokens-status :active-theme-ids #{}
:active-set-ids #{})]
(t/is (empty? (cfo/get-active-themes tokens-status tokens-lib)))))
(t/testing "returns empty sequence when tokens-lib has no themes"
(let [tokens-lib (ctob/make-tokens-lib)
tokens-status (ctos/make-tokens-status)]
(t/is (empty? (cfo/get-active-themes tokens-status tokens-lib)))))
(t/testing "active theme ids in status that do not exist in lib are silently ignored"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) :name "theme-1" :group "")))
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1) (thi/new-id! :nonexistent)}
:active-set-ids #{})]
(t/is (= 1 (count (cfo/get-active-themes tokens-status tokens-lib)))))))
(t/deftest test-activate-theme
(t/testing "activating a theme sets it as active"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status)
tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/id :theme-1))]
(t/is (ctos/theme-active? tokens-status' (thi/id :theme-1)))))
(t/testing "activating a theme deactivates other themes in the same group"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group "colors"
:sets #{"set-a"}))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2)
:name "theme-2"
:group "colors"
:sets #{"set-b"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)}
:active-set-ids #{(thi/id :set-a)})
tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/id :theme-2))]
(t/is (ctos/theme-active? tokens-status' (thi/id :theme-2)))
(t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1))))))
(t/testing "themes in different groups can be active simultaneously"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group "colors"
:sets #{"set-a"}))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2)
:name "theme-2"
:group "spacing"
:sets #{"set-b"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)}
:active-set-ids #{(thi/id :set-a)})
tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/id :theme-2))]
(t/is (ctos/theme-active? tokens-status' (thi/id :theme-1)))
(t/is (ctos/theme-active? tokens-status' (thi/id :theme-2)))))
(t/testing "activating an already-active theme returns status unchanged"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)}
:active-set-ids #{(thi/id :set-a)})
tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/id :theme-1))]
(t/is (identical? tokens-status tokens-status'))))
(t/testing "activating a non-existent theme returns status unchanged"
(let [tokens-lib (ctob/make-tokens-lib)
tokens-status (ctos/make-tokens-status)
tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/new-id! :nonexistent))]
(t/is (identical? tokens-status tokens-status')))))
(t/deftest test-deactivate-theme
(t/testing "deactivating an active theme removes it from active themes"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)}
:active-set-ids #{(thi/id :set-a)})
tokens-status' (cfo/deactivate-theme tokens-status tokens-lib (thi/id :theme-1))]
(t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1))))))
(t/testing "deactivating a theme removes sets not used by remaining active themes"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"}))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2)
:name "theme-2"
:group ""
:sets #{"set-b"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1) (thi/id :theme-2)}
:active-set-ids #{(thi/id :set-a) (thi/id :set-b)})
tokens-status' (cfo/deactivate-theme tokens-status tokens-lib (thi/id :theme-1))]
(t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1))))
(t/is (ctos/theme-active? tokens-status' (thi/id :theme-2)))))
(t/testing "deactivating an already-inactive theme returns status unchanged"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group "")))
tokens-status (ctos/make-tokens-status)
tokens-status' (cfo/deactivate-theme tokens-status tokens-lib (thi/id :theme-1))]
(t/is (identical? tokens-status tokens-status'))))
(t/testing "deactivating a non-existent theme returns status unchanged"
(let [tokens-lib (ctob/make-tokens-lib)
tokens-status (ctos/make-tokens-status)
tokens-status' (cfo/deactivate-theme tokens-status tokens-lib (thi/new-id! :nonexistent))]
(t/is (identical? tokens-status tokens-status')))))
(t/deftest test-toggle-theme-active
(t/testing "activates an inactive theme"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status)
tokens-status' (cfo/toggle-theme-active tokens-status tokens-lib (thi/id :theme-1))]
(t/is (ctos/theme-active? tokens-status' (thi/id :theme-1)))))
(t/testing "deactivates an active theme"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)}
:active-set-ids #{(thi/id :set-a)})
tokens-status' (cfo/toggle-theme-active tokens-status tokens-lib (thi/id :theme-1))]
(t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1)))))))
(t/deftest test-set-theme-active
(t/testing "activates a theme when active? is true"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status)
tokens-status' (cfo/set-theme-active tokens-status tokens-lib (thi/id :theme-1) true)]
(t/is (ctos/theme-active? tokens-status' (thi/id :theme-1)))))
(t/testing "deactivates a theme when active? is false"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1)
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)}
:active-set-ids #{(thi/id :set-a)})
tokens-status' (cfo/set-theme-active tokens-status tokens-lib (thi/id :theme-1) false)]
(t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1)))))))
(t/deftest test-get-active-sets
(t/testing "returns active sets resolved from status and lib"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b")))
tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id})
active-sets (cfo/get-active-sets tokens-status tokens-lib)]
(t/is (= 1 (count active-sets)))
(t/is (= set-a-id (ctob/get-id (first active-sets))))))
(t/testing "returns empty set when no sets are active"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")))
tokens-status (ctos/make-tokens-status)
active-sets (cfo/get-active-sets tokens-status tokens-lib)]
(t/is (empty? active-sets)))))
(t/deftest test-set-set-active
(t/testing "activates a set and clears all themes"
(let [set-a-id (thi/new-id! :set-a)
theme-1-id (thi/new-id! :theme-1)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{})
tokens-status' (cfo/set-set-active tokens-status tokens-lib set-a-id true)]
(t/is (ctos/set-active? tokens-status' set-a-id))
(t/is (= #{} (ctos/get-active-theme-ids tokens-status')))))
(t/testing "deactivates a set and clears all themes"
(let [set-a-id (thi/new-id! :set-a)
theme-1-id (thi/new-id! :theme-1)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{set-a-id})
tokens-status' (cfo/set-set-active tokens-status tokens-lib set-a-id false)]
(t/is (not (ctos/set-active? tokens-status' set-a-id)))
(t/is (= #{} (ctos/get-active-theme-ids tokens-status')))))
(t/testing "returns same status when set is already in target state"
(let [set-a-id (thi/new-id! :set-a)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")))
tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id})
tokens-status' (cfo/set-set-active tokens-status tokens-lib set-a-id true)]
(t/is (identical? tokens-status tokens-status'))))
(t/testing "returns same status for non-existent set"
(let [tokens-lib (ctob/make-tokens-lib)
tokens-status (ctos/make-tokens-status)
tokens-status' (cfo/set-set-active tokens-status tokens-lib (thi/new-id! :nonexistent) true)]
(t/is (identical? tokens-status tokens-status')))))
(t/deftest test-activate-set
(t/testing "delegates to set-set-active with true"
(let [set-a-id (thi/new-id! :set-a)
theme-1-id (thi/new-id! :theme-1)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status)
tokens-status' (cfo/activate-set tokens-status tokens-lib set-a-id)]
(t/is (ctos/set-active? tokens-status' set-a-id))
(t/is (= #{} (ctos/get-active-theme-ids tokens-status'))))))
(t/deftest test-deactivate-set
(t/testing "delegates to set-set-active with false"
(let [set-a-id (thi/new-id! :set-a)
theme-1-id (thi/new-id! :theme-1)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{set-a-id})
tokens-status' (cfo/deactivate-set tokens-status tokens-lib set-a-id)]
(t/is (not (ctos/set-active? tokens-status' set-a-id)))
(t/is (= #{} (ctos/get-active-theme-ids tokens-status'))))))
(t/deftest test-toggle-set-active
(t/testing "activates an inactive set"
(let [set-a-id (thi/new-id! :set-a)
theme-1-id (thi/new-id! :theme-1)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{})
tokens-status' (cfo/toggle-set-active tokens-status tokens-lib set-a-id)]
(t/is (ctos/set-active? tokens-status' set-a-id))
(t/is (= #{} (ctos/get-active-theme-ids tokens-status')))))
(t/testing "deactivates an active set"
(let [set-a-id (thi/new-id! :set-a)
theme-1-id (thi/new-id! :theme-1)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{set-a-id})
tokens-status' (cfo/toggle-set-active tokens-status tokens-lib set-a-id)]
(t/is (not (ctos/set-active? tokens-status' set-a-id)))
(t/is (= #{} (ctos/get-active-theme-ids tokens-status'))))))
(t/deftest test-sets-at-path-all-active?
(t/testing "returns :all when all sets at path are active"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b")))
tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id set-b-id})]
(t/is (= :all (cfo/sets-at-path-all-active? tokens-status tokens-lib ["group"])))))
(t/testing "returns :none when no sets at path are active"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b")))
tokens-status (ctos/make-tokens-status :active-set-ids #{})]
(t/is (= :none (cfo/sets-at-path-all-active? tokens-status tokens-lib ["group"])))))
(t/testing "returns :partial when some sets at path are active"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b")))
tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id})]
(t/is (= :partial (cfo/sets-at-path-all-active? tokens-status tokens-lib ["group"])))))
(t/testing "returns :none for empty path when no sets exist at root"
(let [tokens-lib (ctob/make-tokens-lib)
tokens-status (ctos/make-tokens-status)]
(t/is (= :none (cfo/sets-at-path-all-active? tokens-status tokens-lib [])))))
(t/testing "several combinations with longer paths"
(let [baz-id (thi/new-id! :baz)
bam-id (thi/new-id! :bam)
none-id (thi/new-id! :none)
partial-id (thi/new-id! :partial)
all-id (thi/new-id! :all)
invalid-id (thi/new-id! :invalid)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id baz-id :name "foo/bar/baz"))
(ctob/add-set (ctob/make-token-set :id bam-id :name "foo/bar/bam"))
(ctob/add-theme (ctob/make-token-theme :id none-id
:name "none"))
(ctob/add-theme (ctob/make-token-theme :id partial-id
:name "partial"
:sets #{"foo/bar/baz"}))
(ctob/add-theme (ctob/make-token-theme :id all-id
:name "all"
:sets #{"foo/bar/baz"
"foo/bar/bam"}))
(ctob/add-theme (ctob/make-token-theme :id invalid-id
:name "invalid"
:sets #{"foo/missing"})))
expected-none (-> (ctos/make-tokens-status {:active-theme-ids #{none-id}
:active-set-ids #{}})
(cfo/sets-at-path-all-active? tokens-lib ["foo"]))
expected-all (-> (ctos/make-tokens-status {:active-theme-ids #{all-id}
:active-set-ids #{baz-id bam-id}})
(cfo/sets-at-path-all-active? tokens-lib ["foo"]))
expected-partial (-> (ctos/make-tokens-status {:active-theme-ids #{partial-id}
:active-set-ids #{baz-id}})
(cfo/sets-at-path-all-active? tokens-lib ["foo"]))
expected-invalid-none (-> (ctos/make-tokens-status {:active-theme-ids #{invalid-id}
:active-set-ids #{}})
(cfo/sets-at-path-all-active? tokens-lib ["foo"]))]
(t/is (= :none expected-none))
(t/is (= :all expected-all))
(t/is (= :partial expected-partial))
(t/is (= :none expected-invalid-none)))))
(t/deftest test-toggle-set-group-active
(t/testing "activates all sets in group when none are active, and deactivate all themes"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
set-c-id (thi/new-id! :set-c)
theme-1-id (thi/new-id! :theme-1)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b"))
(ctob/add-set (ctob/make-token-set :id set-c-id :name "group/set-b/child"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{})
tokens-status' (cfo/toggle-set-group-active tokens-status tokens-lib ["group"])]
(t/is (ctos/set-active? tokens-status' set-a-id))
(t/is (ctos/set-active? tokens-status' set-b-id))
(t/is (ctos/set-active? tokens-status' set-c-id))
(t/is (= #{} (ctos/get-active-theme-ids tokens-status')))))
(t/testing "deactivates all sets in group when all are active, and deactivate all themes"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
set-c-id (thi/new-id! :set-c)
theme-1-id (thi/new-id! :theme-1)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b"))
(ctob/add-set (ctob/make-token-set :id set-c-id :name "group/set-b/child"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"group/set-a" "group/set-b" "group/set-b/child"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{set-a-id set-b-id set-c-id})
tokens-status' (cfo/toggle-set-group-active tokens-status tokens-lib ["group"])]
(t/is (not (ctos/set-active? tokens-status' set-a-id)))
(t/is (not (ctos/set-active? tokens-status' set-b-id)))
(t/is (not (ctos/set-active? tokens-status' set-c-id)))
(t/is (= #{} (ctos/get-active-theme-ids tokens-status')))))
(t/testing "deactivates all sets in group when only some are active, and deactivate all themes"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
set-c-id (thi/new-id! :set-c)
theme-1-id (thi/new-id! :theme-1)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b"))
(ctob/add-set (ctob/make-token-set :id set-c-id :name "group/set-b/child"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{set-a-id})
tokens-status' (cfo/toggle-set-group-active tokens-status tokens-lib ["group"])]
(t/is (not (ctos/set-active? tokens-status' set-a-id)))
(t/is (not (ctos/set-active? tokens-status' set-b-id)))
(t/is (not (ctos/set-active? tokens-status' set-c-id)))
(t/is (= #{} (ctos/get-active-theme-ids tokens-status'))))))
(t/deftest test-get-tokens-in-active-sets
(t/testing "returns merged tokens from active sets in set order"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px")
tok-b (ctob/make-token :name "radius" :type :border-radius :value "4px")
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b"))
(ctob/add-token set-a-id tok-a)
(ctob/add-token set-b-id tok-b))
tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id set-b-id})
tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)]
(t/is (= 2 (count tokens)))
(t/is (contains? tokens "spacing"))
(t/is (contains? tokens "radius"))))
(t/testing "later active sets override earlier sets with same token name"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px")
tok-b (ctob/make-token :name "spacing" :type :spacing :value "16px")
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b"))
(ctob/add-token set-a-id tok-a)
(ctob/add-token set-b-id tok-b))
tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id set-b-id})
tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)]
(t/is (= 1 (count tokens)))
(t/is (= "16px" (:value (get tokens "spacing"))))))
(t/testing "tokens in inactive sets are not included"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px")
tok-b (ctob/make-token :name "spacing" :type :spacing :value "16px")
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b"))
(ctob/add-token set-a-id tok-a)
(ctob/add-token set-b-id tok-b))
tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id})
tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)]
(t/is (= 1 (count tokens)))
(t/is (= "8px" (:value (get tokens "spacing"))))))
(t/testing "returns empty map when no sets are active"
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")))
tokens-status (ctos/make-tokens-status)
tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)]
(t/is (empty? tokens))))
(t/testing "returns empty map when empty lib"
(let [tokens-lib (ctob/make-tokens-lib)
tokens-status (ctos/make-tokens-status)
tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)]
(t/is (empty? tokens)))))
(t/deftest test-get-tokens-in-active-sets-force
(t/testing "includes force-set-id even when not in active sets"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px")
tok-b (ctob/make-token :name "radius" :type :border-radius :value "4px")
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b"))
(ctob/add-token set-a-id tok-a)
(ctob/add-token set-b-id tok-b))
tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id})
tokens (cfo/get-tokens-in-active-sets-force tokens-status tokens-lib set-b-id)]
(t/is (= 2 (count tokens)))
(t/is (contains? tokens "spacing"))
(t/is (contains? tokens "radius"))))
(t/testing "active set tokens override force-set tokens with same name"
(let [set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px")
tok-b (ctob/make-token :name "spacing" :type :spacing :value "32px")
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b"))
(ctob/add-token set-a-id tok-a)
(ctob/add-token set-b-id tok-b))
tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id})
tokens (cfo/get-tokens-in-active-sets-force tokens-status tokens-lib set-b-id)]
(t/is (= 1 (count tokens)))
(t/is (= "8px" (:value (get tokens "spacing")))))))
(t/deftest test-sync-tokens-status-with-lib
(t/testing "removes theme ids that no longer exist in the lib"
(let [theme-1-id (thi/new-id! :theme-1)
set-a-id (thi/new-id! :set-a)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id (thi/new-id! :removed-theme)}
:active-set-ids #{set-a-id})
tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)]
(t/is (= #{theme-1-id} (ctos/get-active-theme-ids tokens-status')))
(t/is (ctos/theme-active? tokens-status' theme-1-id))))
(t/testing "removes set ids that no longer exist in the lib (active themes)"
(let [theme-1-id (thi/new-id! :theme-1)
set-a-id (thi/new-id! :set-a)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{set-a-id (thi/new-id! :removed-set)})
tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)]
(t/is (= #{set-a-id} (ctos/get-active-set-ids tokens-status')))))
(t/testing "removes set ids that no longer exist in the lib (no active themes)"
(let [theme-1-id (thi/new-id! :theme-1)
set-a-id (thi/new-id! :set-a)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{}
:active-set-ids #{set-a-id (thi/new-id! :removed-set)})
tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)]
(t/is (= #{set-a-id} (ctos/get-active-set-ids tokens-status')))))
(t/testing "updates active sets when theme changes"
(let [theme-1-id (thi/new-id! :theme-1)
set-a-id (thi/new-id! :set-a)
set-b-id (thi/new-id! :set-b)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-b"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{set-a-id (thi/new-id! :removed-set)})
tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)]
(t/is (= #{set-b-id} (ctos/get-active-set-ids tokens-status')))))
(t/testing "returns same status object when everything is valid"
(let [theme-1-id (thi/new-id! :theme-1)
set-a-id (thi/new-id! :set-a)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))
(ctob/add-theme (ctob/make-token-theme :id theme-1-id
:name "theme-1"
:group ""
:sets #{"set-a"})))
tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id}
:active-set-ids #{set-a-id})
tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)]
(t/is (identical? tokens-status tokens-status'))))
(t/testing "returns same status object when both theme and set ids are empty"
(let [tokens-lib (ctob/make-tokens-lib)
tokens-status (ctos/make-tokens-status)
tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)]
(t/is (identical? tokens-status tokens-status'))))
(t/testing "removes all themes and sets when lib is empty"
(let [tokens-lib (ctob/make-tokens-lib)
tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/new-id! :old-theme)}
:active-set-ids #{(thi/new-id! :old-set)})
tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)]
(t/is (= #{} (ctos/get-active-theme-ids tokens-status')))
(t/is (= #{} (ctos/get-active-set-ids tokens-status'))))))
;; Mismatched tokens-status and tokens-lib
;;
;; The tokens-status may contain IDs for themes and sets that do not exist in
;; the tokens-lib (e.g. when the status comes from the tokens lib of a different source).
;; All functions must handle this gracefully: return a reasonable result, never
;; throw an exception.
(t/deftest test-mismatched-status-lib
(let [real-theme-1 (thi/new-id! :real-theme-1)
real-theme-2 (thi/new-id! :real-theme-2)
real-set-a (thi/new-id! :real-set-a)
real-set-b (thi/new-id! :real-set-b)
real-set-c (thi/new-id! :real-set-c)
invalid-theme-id (thi/new-id! :invalid-theme)
invalid-set-id (thi/new-id! :invalid-set)
tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :id real-set-a :name "real-set-a"))
(ctob/add-set (ctob/make-token-set :id real-set-b :name "real-set-b"))
(ctob/add-set (ctob/make-token-set :id real-set-c :name "real-set-c"))
(ctob/add-token real-set-a (ctob/make-token :name "spacing" :type :spacing :value "8px"))
(ctob/add-token real-set-b (ctob/make-token :name "radius" :type :border-radius :value "4px"))
(ctob/add-theme (ctob/make-token-theme :id real-theme-1
:name "theme-1"
:group "colors"
:sets #{"real-set-a"}))
(ctob/add-theme (ctob/make-token-theme :id real-theme-2
:name "theme-2"
:group "colors"
:sets #{"real-set-b" "real-set-c"})))
;; Status has one valid and one invalid theme, one valid and one invalid set
tokens-status (ctos/make-tokens-status :active-theme-ids #{real-theme-1 invalid-theme-id}
:active-set-ids #{real-set-a invalid-set-id})]
(t/testing "activate-theme with non-existent theme id returns status unchanged"
(let [status' (cfo/activate-theme tokens-status tokens-lib invalid-theme-id)]
(t/is (identical? tokens-status status'))))
(t/testing "activate-theme with valid theme works despite invalid ids in status"
(let [status' (cfo/activate-theme tokens-status tokens-lib real-theme-2)]
(t/is (ctos/theme-active? status' real-theme-2))
(t/is (not (ctos/theme-active? status' real-theme-1)))
(t/is (ctos/theme-active? status' invalid-theme-id))
(t/is (contains? (ctos/get-active-set-ids status') real-set-b))
(t/is (contains? (ctos/get-active-set-ids status') real-set-c))))
(t/testing "deactivate-theme with non-existent theme id recalculates sets"
(let [status' (cfo/deactivate-theme tokens-status tokens-lib invalid-theme-id)]
(t/is (not (ctos/theme-active? status' invalid-theme-id)))
(t/is (ctos/theme-active? status' real-theme-1))
(t/is (contains? (ctos/get-active-set-ids status') real-set-a))))
(t/testing "toggle-theme-active with non-existent theme id deactivates it"
(let [status' (cfo/toggle-theme-active tokens-status tokens-lib invalid-theme-id)]
(t/is (not (ctos/theme-active? status' invalid-theme-id)))
(t/is (ctos/theme-active? status' real-theme-1))))
(t/testing "toggle-theme-active with valid inactive theme activates it"
(let [status' (cfo/toggle-theme-active tokens-status tokens-lib real-theme-2)]
(t/is (ctos/theme-active? status' real-theme-2))
(t/is (contains? (ctos/get-active-set-ids status') real-set-b))
(t/is (contains? (ctos/get-active-set-ids status') real-set-c))))
(t/testing "set-theme-active false with non-existent theme recalculates sets"
(let [status' (cfo/set-theme-active tokens-status tokens-lib invalid-theme-id false)]
(t/is (not (ctos/theme-active? status' invalid-theme-id)))
(t/is (contains? (ctos/get-active-set-ids status') real-set-a))))
(t/testing "set-theme-active true with non-existent theme returns status unchanged"
(let [status' (cfo/set-theme-active tokens-status tokens-lib invalid-theme-id true)]
(t/is (identical? tokens-status status'))))
(t/testing "get-active-themes filters out non-existent theme ids"
(let [active-themes (cfo/get-active-themes tokens-status tokens-lib)]
(t/is (= 1 (count active-themes)))
(t/is (= real-theme-1 (ctob/get-id (first active-themes))))))
(t/testing "get-active-sets filters out non-existent set ids"
(let [active-sets (cfo/get-active-sets tokens-status tokens-lib)]
(t/is (= 1 (count active-sets)))
(t/is (= real-set-a (ctob/get-id (first active-sets))))))
(t/testing "set-set-active with non-existent set id returns status unchanged"
(t/is (identical? tokens-status (cfo/set-set-active tokens-status tokens-lib invalid-set-id true)))
(t/is (identical? tokens-status (cfo/set-set-active tokens-status tokens-lib invalid-set-id false))))
(t/testing "activate-set with non-existent set id returns status unchanged"
(t/is (identical? tokens-status (cfo/activate-set tokens-status tokens-lib invalid-set-id))))
(t/testing "deactivate-set with non-existent set id returns status unchanged"
(t/is (identical? tokens-status (cfo/deactivate-set tokens-status tokens-lib invalid-set-id))))
(t/testing "toggle-set-active with non-existent set id returns status unchanged"
(t/is (identical? tokens-status (cfo/toggle-set-active tokens-status tokens-lib invalid-set-id))))
(t/testing "sets-at-path-all-active? with invalid set ids in status don't cause errors"
;; real-set-a is at [] and is active (plus invalid-set-id); real-set-b, real-set-c at [] inactive
(t/is (= :partial (cfo/sets-at-path-all-active? tokens-status tokens-lib [])))
;; All sets at [] active (with invalid id still in status)
(let [all-active-status (ctos/make-tokens-status :active-theme-ids #{}
:active-set-ids #{real-set-a real-set-b real-set-c invalid-set-id})]
(t/is (= :all (cfo/sets-at-path-all-active? all-active-status tokens-lib [])))))
(t/testing "toggle-set-group-active with invalid set ids in status don't cause errors"
;; Status has real-set-a + invalid-set-id active -> :partial at []
;; Toggling :partial deactivates all at [], invalid-set-id remains
(let [status' (cfo/toggle-set-group-active tokens-status tokens-lib [])]
(t/is (not (ctos/set-active? status' real-set-a)))
(t/is (not (ctos/set-active? status' real-set-b)))
(t/is (not (ctos/set-active? status' real-set-c)))
(t/is (ctos/set-active? status' invalid-set-id)))
;; All sets at [] active + invalid-set-id -> :all at []
;; Toggling :all deactivates all at [], invalid-set-id remains
(let [all-active (ctos/make-tokens-status :active-theme-ids #{}
:active-set-ids #{real-set-a real-set-b real-set-c invalid-set-id})
status' (cfo/toggle-set-group-active all-active tokens-lib [])]
(t/is (not (ctos/set-active? status' real-set-a)))
(t/is (not (ctos/set-active? status' real-set-b)))
(t/is (not (ctos/set-active? status' real-set-c)))
(t/is (ctos/set-active? status' invalid-set-id))))
(t/testing "get-tokens-in-active-sets with invalid set ids returns tokens from valid sets only"
(let [tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)]
(t/is (= 1 (count tokens)))
(t/is (contains? tokens "spacing"))
(t/is (not (contains? tokens "radius")))))
(t/testing "get-tokens-in-active-sets-force with invalid set ids and valid force set"
(let [tokens (cfo/get-tokens-in-active-sets-force tokens-status tokens-lib real-set-b)]
(t/is (= 2 (count tokens)))
(t/is (contains? tokens "spacing"))
(t/is (contains? tokens "radius"))))
(t/testing "get-tokens-in-active-sets-force with non-existent force set id"
(let [tokens (cfo/get-tokens-in-active-sets-force tokens-status tokens-lib invalid-set-id)]
(t/is (= 1 (count tokens)))
(t/is (contains? tokens "spacing"))))
(t/testing "all-invalid status: every id is non-existent in lib"
(let [all-invalid (ctos/make-tokens-status :active-theme-ids #{invalid-theme-id}
:active-set-ids #{invalid-set-id})]
;; get-active-themes returns empty
(t/is (empty? (cfo/get-active-themes all-invalid tokens-lib)))
;; get-active-sets returns empty
(t/is (empty? (cfo/get-active-sets all-invalid tokens-lib)))
;; get-tokens-in-active-sets returns empty
(t/is (empty? (cfo/get-tokens-in-active-sets all-invalid tokens-lib)))
;; get-tokens-in-active-sets-force returns only force set tokens
(let [tokens (cfo/get-tokens-in-active-sets-force all-invalid tokens-lib real-set-a)]
(t/is (= 1 (count tokens)))
(t/is (contains? tokens "spacing")))
;; activate-theme for non-existent id is no-op
(t/is (identical? all-invalid (cfo/activate-theme all-invalid tokens-lib invalid-theme-id)))
;; deactivate-theme removes non-existent id and recalculates (empty) sets
(let [status' (cfo/deactivate-theme all-invalid tokens-lib invalid-theme-id)]
(t/is (not (ctos/theme-active? status' invalid-theme-id)))
(t/is (= #{} (ctos/get-active-set-ids status'))))
;; toggle-theme-active toggles non-existent id (deactivates)
(let [status' (cfo/toggle-theme-active all-invalid tokens-lib invalid-theme-id)]
(t/is (not (ctos/theme-active? status' invalid-theme-id))))
;; set-theme-active false removes non-existent id
(let [status' (cfo/set-theme-active all-invalid tokens-lib invalid-theme-id false)]
(t/is (not (ctos/theme-active? status' invalid-theme-id))))))))
;; absorb-tokens-lib
(defn- make-sample-tokens-lib
"A tokens-lib with a set (and a token in it) for each label. Each set
is named as its label, and its id is registered in thi under it."
[& set-labels]
(reduce (fn [tokens-lib set-label]
(let [set-id (thi/new-id! set-label)]
(-> tokens-lib
(ctob/add-set (ctob/make-token-set :id set-id
:name (name set-label)))
(ctob/add-token set-id (ctob/make-token :name (str (name set-label) "-token")
:type :border-radius
:value 10)))))
(ctob/make-tokens-lib)
set-labels))
(defn- make-sample-tokens-status
"A tokens-status with the sets of the given labels active. The sets
must have been created before by make-sample-tokens-lib."
[& set-labels]
(ctos/make-tokens-status :active-set-ids (into #{} (map thi/id) set-labels)))
(defn- make-sample-data
"File data with the given tokens-lib, tokens-status and tokens-source.
Those not given are left unset."
[label & {:keys [tokens-lib tokens-status tokens-source]}]
(-> (ctf/file-data (thf/sample-file label))
(cfo/ensure-tokens-lib)
(cfo/update-tokens-lib (constantly tokens-lib))
(cfo/update-tokens-status (constantly tokens-status))
(cfo/set-tokens-source tokens-source)))
(t/deftest test-absorb-tokens-lib
(t/testing "copies the tokens-lib of the library and unsets the tokens-source"
(let [library-data (make-sample-data :library
:tokens-lib (make-sample-tokens-lib :lib-set))
file-data (make-sample-data :file1
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= (cfo/get-tokens-lib library-data) (cfo/get-tokens-lib file-data')))
(t/is (nil? (cfo/get-tokens-source file-data')))
(t/is (true? (cfo/effective-tokens-source? file-data' (:id file-data'))))
(t/is (true? (cfo/editable-tokens? file-data')))))
(t/testing "replaces the own tokens-lib of the file"
(let [library-data (make-sample-data :library
:tokens-lib (make-sample-tokens-lib :lib-set))
file-data (make-sample-data :file1
:tokens-lib (make-sample-tokens-lib :own-set)
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= (cfo/get-tokens-lib library-data) (cfo/get-tokens-lib file-data')))
(t/is (nil? (cfo/get-tokens-source file-data')))))
(t/testing "copies an empty tokens-lib of the library"
(let [empty-lib (ctob/make-tokens-lib)
library-data (make-sample-data :library :tokens-lib empty-lib)
file-data (make-sample-data :file1
:tokens-lib (make-sample-tokens-lib :own-set)
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= empty-lib (cfo/get-tokens-lib file-data')))
(t/is (nil? (cfo/get-tokens-source file-data')))))
(t/testing "unsets the tokens-source even if the file had no tokens-source"
(let [library-data (make-sample-data :library
:tokens-lib (make-sample-tokens-lib :lib-set))
file-data (make-sample-data :file1)
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= (cfo/get-tokens-lib library-data) (cfo/get-tokens-lib file-data')))
(t/is (nil? (cfo/get-tokens-source file-data')))))
(t/testing "does not change anything else in the file"
(let [library-data (make-sample-data :library
:tokens-lib (make-sample-tokens-lib :lib-set))
file-data (make-sample-data :file1
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= (dissoc file-data :tokens-lib :tokens-status :tokens-source)
(dissoc file-data' :tokens-lib :tokens-status :tokens-source))))))
(t/deftest test-absorb-tokens-lib-status
(t/testing "preserves the tokens-status of the file"
(let [library-lib (make-sample-tokens-lib :lib-set :other-set)
file-status (make-sample-tokens-status :lib-set)
library-status (make-sample-tokens-status :other-set)
library-data (make-sample-data :library
:tokens-lib library-lib
:tokens-status library-status)
file-data (make-sample-data :file1
:tokens-status file-status
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= file-status (cfo/get-tokens-status file-data')))
(t/is (not= library-status (cfo/get-tokens-status file-data')))))
(t/testing "preserves an empty tokens-status of the file"
(let [library-lib (make-sample-tokens-lib :lib-set)
file-status (ctos/make-tokens-status)
library-status (make-sample-tokens-status :lib-set)
library-data (make-sample-data :library
:tokens-lib library-lib
:tokens-status library-status)
file-data (make-sample-data :file1
:tokens-status file-status
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= file-status (cfo/get-tokens-status file-data')))))
(t/testing "copies the tokens-status of the library if the file has none"
(let [library-lib (make-sample-tokens-lib :lib-set)
library-status (make-sample-tokens-status :lib-set)
library-data (make-sample-data :library
:tokens-lib library-lib
:tokens-status library-status)
file-data (make-sample-data :file1
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= library-status (cfo/get-tokens-status file-data')))
(t/is (= #{(thi/id :lib-set)}
(ctos/get-active-set-ids (cfo/get-tokens-status file-data'))))
(t/is (some? (ctob/get-set (cfo/get-tokens-lib file-data') (thi/id :lib-set))))))
(t/testing "leaves the tokens-status unset if neither the file nor the library have one"
(let [library-data (make-sample-data :library
:tokens-lib (make-sample-tokens-lib :lib-set))
file-data (make-sample-data :file1
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (nil? (cfo/get-tokens-status file-data'))))))
(t/deftest test-absorb-tokens-lib-library-without-tokens
(t/testing "library without tokens-lib: keeps the tokens-lib and tokens-status of the file, and unsets the tokens-source"
(let [file-lib (make-sample-tokens-lib :own-set)
file-status (make-sample-tokens-status :own-set)
library-status (ctos/make-tokens-status :active-set-ids #{(uuid/next)})
library-data (make-sample-data :library
:tokens-status library-status)
file-data (make-sample-data :file1
:tokens-lib file-lib
:tokens-status file-status
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= file-lib (cfo/get-tokens-lib file-data')))
(t/is (= file-status (cfo/get-tokens-status file-data')))
(t/is (nil? (cfo/get-tokens-source file-data')))))
(t/testing "library without tokens-lib: does not copy the tokens-status of the library"
(let [library-status (ctos/make-tokens-status :active-set-ids #{(uuid/next)})
library-data (make-sample-data :library
:tokens-status library-status)
file-data (make-sample-data :file1
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (nil? (cfo/get-tokens-lib file-data')))
(t/is (nil? (cfo/get-tokens-status file-data')))
(t/is (nil? (cfo/get-tokens-source file-data')))))
(t/testing "library without any tokens attribute"
(let [library-data (ctf/file-data (thf/sample-file :library))
file-lib (make-sample-tokens-lib :own-set)
file-data (make-sample-data :file1
:tokens-lib file-lib
:tokens-source (:id library-data))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (= file-lib (cfo/get-tokens-lib file-data')))
(t/is (nil? (cfo/get-tokens-source file-data')))))
(t/testing "neither the file nor the library have tokens"
(let [library-data (ctf/file-data (thf/sample-file :library))
file-data (ctf/file-data (thf/sample-file :file1))
file-data' (cfo/absorb-tokens-lib file-data library-data)]
(t/is (nil? (cfo/get-tokens-lib file-data')))
(t/is (nil? (cfo/get-tokens-status file-data')))
(t/is (nil? (cfo/get-tokens-source file-data'))))))
(t/deftest test-absorb-tokens-lib-no-library-data
(t/testing "nil library-data: the file is returned unchanged"
(let [file-lib (make-sample-tokens-lib :own-set)
file-data (make-sample-data :file1
:tokens-lib file-lib
:tokens-status (make-sample-tokens-status :own-set)
:tokens-source (uuid/next))]
(t/is (identical? file-data (cfo/absorb-tokens-lib file-data nil)))))
(t/testing "nil library-data and a file without tokens"
(let [file-data (ctf/file-data (thf/sample-file :file1))]
(t/is (identical? file-data (cfo/absorb-tokens-lib file-data nil))))))
(t/deftest test-detach-external-references-preserves-own-tokens
(let [file (thf/sample-file :file1)
lib (make-sample-tokens-lib :own-set)
status (make-sample-tokens-status :own-set)
file (-> file
(assoc-in [:data :tokens-lib] lib)
(assoc-in [:data :tokens-status] status))]
(doseq [source [nil (:id file)]]
(t/testing (str "local tokens source " source)
(let [file (cond-> file
source (assoc-in [:data :tokens-source] source))
detached (ctf/detach-external-references file (:id file))]
(t/is (= source (cfo/get-tokens-source (:data detached))))
(t/is (identical? lib (cfo/get-tokens-lib (:data detached))))
(t/is (= status (cfo/get-tokens-status (:data detached)))))))))
(t/deftest test-detach-external-references-removes-external-tokens-status
(let [file (-> (thf/sample-file :file1)
(assoc-in [:data :tokens-source] (uuid/next))
(assoc-in [:data :tokens-status] (ctos/make-tokens-status)))
detached (ctf/detach-external-references file (:id file))]
(t/is (not (contains? (:data detached) :tokens-source)))
(t/is (not (contains? (:data detached) :tokens-status)))
(t/is (= detached (ctf/check-file detached)))))