mirror of
https://github.com/penpot/penpot.git
synced 2026-10-03 09:16:15 +00:00
Creating a typography from a text whose font is no longer installed baked the broken font-id into a new asset. With several texts selected there is no single style to capture either. The add-typography event now does nothing in both cases, and the "Add typography" button in the local library is disabled with a label that explains why. The button lives in its own component so selection, shape and editor changes re-render only the button, not the typography list, and shared libraries do not subscribe to that state at all. The event and the button read the editor data through the new `editor-text-options`, so both see the same font for the wasm, v2 and v1 text editors. AI-assisted-by: claude-opus-5-5
634 lines
31 KiB
Clojure
634 lines
31 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 frontend-tests.data.workspace-texts-test
|
|
(:require
|
|
[app.common.geom.rect :as grc]
|
|
[app.common.test-helpers.files :as cthf]
|
|
[app.common.test-helpers.shapes :as cths]
|
|
[app.common.types.modifiers :as ctm]
|
|
[app.common.types.shape :as cts]
|
|
[app.common.types.text :as txt]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.workspace.texts :as dwt]
|
|
[app.main.data.workspace.texts-events :as dwte]
|
|
[app.main.ui.workspace.shapes.text.viewport-texts-html :as vth]
|
|
[cljs.test :as t :include-macros true]
|
|
[frontend-tests.helpers.state :as ths]))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Helpers
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defn- make-text-shape
|
|
"Build a fully initialised text shape at the given position."
|
|
[& {:keys [x y width height position-data]
|
|
:or {x 10 y 20 width 100 height 50}}]
|
|
(cond-> (cts/setup-shape {:type :text
|
|
:x x
|
|
:y y
|
|
:width width
|
|
:height height})
|
|
(some? position-data)
|
|
(assoc :position-data position-data)))
|
|
|
|
(defn- make-degenerate-text-shape
|
|
"Simulate a text shape decoded from the server via map->Rect (which bypasses
|
|
make-rect's 0.01 minimum enforcement), giving it a zero-width / zero-height
|
|
selrect. This is the exact condition that triggered the original crash:
|
|
change-dimensions-modifiers divided by sr-width (== 0), producing an Infinity
|
|
scale factor that propagated through the transform pipeline until
|
|
calculate-selrect / center->rect returned nil, and then gpt/point threw
|
|
'invalid arguments (on pointer constructor)'."
|
|
[& {:keys [x y width height]
|
|
:or {x 10 y 20 width 0 height 0}}]
|
|
(-> (make-text-shape :x x :y y :width 100 :height 50)
|
|
;; Bypass make-rect by constructing the Rect record directly, the same
|
|
;; way decode-rect does during JSON deserialization from the backend.
|
|
(assoc :selrect (grc/map->Rect {:x x :y y
|
|
:width width :height height
|
|
:x1 x :y1 y
|
|
:x2 (+ x width) :y2 (+ y height)}))))
|
|
|
|
(defn- sample-position-data
|
|
"Return a minimal position-data vector with the supplied coords."
|
|
[x y]
|
|
[{:x x :y y :width 80 :height 16 :fills [] :text "hello"}])
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: nil / no-op guard
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-nil-modifier-returns-shape-unchanged
|
|
(t/testing "nil text-modifier returns the original shape untouched"
|
|
(let [shape (make-text-shape)
|
|
result (dwt/apply-text-modifier shape nil)]
|
|
(t/is (= shape result)))))
|
|
|
|
(t/deftest apply-text-modifier-empty-map-no-keys-returns-shape-unchanged
|
|
(t/testing "modifier with no recognised keys leaves shape unchanged"
|
|
(let [shape (make-text-shape)
|
|
modifier {}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= (:selrect shape) (:selrect result)))
|
|
(t/is (= (:width result) (:width shape)))
|
|
(t/is (= (:height result) (:height shape))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: width modifier
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-width-changes-shape-width
|
|
(t/testing "width modifier resizes the shape width"
|
|
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
modifier {:width 200}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= 200.0 (-> result :selrect :width))))))
|
|
|
|
(t/deftest apply-text-modifier-width-nil-skips-width-change
|
|
(t/testing "nil :width in modifier does not alter the width"
|
|
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
modifier {:width nil}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= (-> shape :selrect :width) (-> result :selrect :width))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: height modifier
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-height-changes-shape-height
|
|
(t/testing "height modifier resizes the shape height"
|
|
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
modifier {:height 120}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= 120.0 (-> result :selrect :height))))))
|
|
|
|
(t/deftest apply-text-modifier-height-nil-skips-height-change
|
|
(t/testing "nil :height in modifier does not alter the height"
|
|
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
modifier {:height nil}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= (-> shape :selrect :height) (-> result :selrect :height))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: width + height together
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-width-and-height-both-applied
|
|
(t/testing "both width and height are applied simultaneously"
|
|
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
modifier {:width 300 :height 80}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= 300.0 (-> result :selrect :width)))
|
|
(t/is (= 80.0 (-> result :selrect :height))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: position-data modifier
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-position-data-is-set-on-shape
|
|
(t/testing "position-data modifier replaces the position-data on shape"
|
|
(let [pd (sample-position-data 5 10)
|
|
shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
modifier {:position-data pd}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (some? (:position-data result))))))
|
|
|
|
(t/deftest apply-text-modifier-position-data-nil-leaves-position-data-unchanged
|
|
(t/testing "nil :position-data in modifier does not alter position-data"
|
|
(let [pd (sample-position-data 5 10)
|
|
shape (-> (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
(assoc :position-data pd))
|
|
modifier {:position-data nil}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= pd (:position-data result))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: position-data is translated by delta when shape moves
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-position-data-translated-on-resize
|
|
(t/testing "position-data x/y is adjusted by the delta of the selrect origin"
|
|
(let [pd (sample-position-data 10 20)
|
|
shape (-> (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
(assoc :position-data pd))
|
|
;; Only set position-data; no resize so no origin shift expected
|
|
modifier {:position-data pd}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
;; Delta should be zero (no dimension change), so coords stay the same
|
|
(t/is (= 10.0 (-> result :position-data first :x)))
|
|
(t/is (= 20.0 (-> result :position-data first :y))))))
|
|
|
|
(t/deftest apply-text-modifier-position-data-not-translated-when-nil
|
|
(t/testing "nil position-data on result after modifier is left as nil"
|
|
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
modifier {:width 200}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
;; shape had no position-data; modifier doesn't set one — stays nil
|
|
(t/is (nil? (:position-data result))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: degenerate selrect (zero width or height decoded from the server)
|
|
;;
|
|
;; Root cause of the original crash:
|
|
;; change-dimensions-modifiers divided by (:width selrect) or (:height selrect)
|
|
;; which is 0 when the shape was decoded via map->Rect (bypassing make-rect's
|
|
;; 0.01 minimum), producing Infinity → transform pipeline returned nil selrect
|
|
;; → gpt/point threw "invalid arguments (on pointer constructor)".
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-zero-width-selrect-does-not-throw
|
|
(t/testing "width modifier on a shape with zero selrect width does not throw"
|
|
;; Simulates a shape received from the server whose selrect has width=0
|
|
;; (map->Rect bypasses the 0.01 floor of make-rect).
|
|
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 50)
|
|
modifier {:width 200}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (some? result))
|
|
(t/is (some? (:selrect result))))))
|
|
|
|
(t/deftest apply-text-modifier-zero-height-selrect-does-not-throw
|
|
(t/testing "height modifier on a shape with zero selrect height does not throw"
|
|
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 100 :height 0)
|
|
modifier {:height 80}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (some? result))
|
|
(t/is (some? (:selrect result))))))
|
|
|
|
(t/deftest apply-text-modifier-zero-width-and-height-selrect-does-not-throw
|
|
(t/testing "both modifiers on a fully-degenerate selrect do not throw"
|
|
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
|
modifier {:width 150 :height 60}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (some? result))
|
|
(t/is (some? (:selrect result))))))
|
|
|
|
(t/deftest apply-text-modifier-zero-width-selrect-result-has-correct-width
|
|
(t/testing "applying width modifier to a zero-width shape yields the requested width"
|
|
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 50)
|
|
modifier {:width 200}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= 200.0 (-> result :selrect :width))))))
|
|
|
|
(t/deftest apply-text-modifier-zero-height-selrect-result-has-correct-height
|
|
(t/testing "applying height modifier to a zero-height shape yields the requested height"
|
|
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 100 :height 0)
|
|
modifier {:height 80}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= 80.0 (-> result :selrect :height))))))
|
|
|
|
(t/deftest apply-text-modifier-nil-modifier-on-degenerate-shape-returns-unchanged
|
|
(t/testing "nil modifier on a zero-selrect shape returns the same shape"
|
|
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
|
result (dwt/apply-text-modifier shape nil)]
|
|
(t/is (identical? shape result)))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: shape origin is preserved when there is no dimension change
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-selrect-origin-preserved-without-resize
|
|
(t/testing "selrect x/y origin does not shift when no dimension changes"
|
|
(let [shape (make-text-shape :x 30 :y 40 :width 100 :height 50)
|
|
modifier {:position-data (sample-position-data 30 40)}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (= (-> shape :selrect :x) (-> result :selrect :x)))
|
|
(t/is (= (-> shape :selrect :y) (-> result :selrect :y))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: returned shape is a proper map-like value
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-returns-shape-with-required-keys
|
|
(t/testing "result always contains the core shape keys"
|
|
(let [shape (make-text-shape :x 0 :y 0 :width 100 :height 50)
|
|
modifier {:width 200 :height 80}
|
|
result (dwt/apply-text-modifier shape modifier)]
|
|
(t/is (some? (:id result)))
|
|
(t/is (some? (:type result)))
|
|
(t/is (some? (:selrect result))))))
|
|
|
|
(t/deftest apply-text-modifier-nil-modifier-returns-same-identity
|
|
(t/testing "nil modifier returns the exact same shape object (identity)"
|
|
(let [shape (make-text-shape)]
|
|
(t/is (identical? shape (dwt/apply-text-modifier shape nil))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: delta-move computation does not throw on degenerate selrect
|
|
;;
|
|
;; The delta-move in apply-text-modifier calls gpt/point on both the
|
|
;; original and new shape selrects. gpt/point throws when given a
|
|
;; non-point-like value (nil, or a map with non-finite :x/:y). Using
|
|
;; ctm/safe-size-rect instead of raw (:selrect …) access ensures a valid
|
|
;; rect is always available for that computation.
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest apply-text-modifier-position-data-with-degenerate-selrect-does-not-throw
|
|
(t/testing "position-data modifier on a zero-selrect shape does not throw"
|
|
(let [pd (sample-position-data 5 10)
|
|
shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
|
result (dwt/apply-text-modifier shape {:position-data pd})]
|
|
(t/is (some? result))
|
|
(t/is (= pd (:position-data result)))))
|
|
|
|
(t/testing "width + position-data modifier on a zero-selrect shape does not throw"
|
|
(let [pd (sample-position-data 5 10)
|
|
shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
|
result (dwt/apply-text-modifier shape {:width 200 :position-data pd})]
|
|
(t/is (some? result))
|
|
(t/is (some? (:selrect result))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: add-typography event normalises float line-height / letter-spacing
|
|
;;
|
|
;; These tests exercise the full add-typography event path in texts.cljs:
|
|
;; a text shape whose content nodes carry float line-height / letter-spacing
|
|
;; values (as produced by JS float arithmetic) must yield a typography entry
|
|
;; in the file with properly-rounded *string* values, not the raw floats.
|
|
;;
|
|
;; Root cause reproduced here: JS float arithmetic (e.g. 1.3 - 0.1) can
|
|
;; produce 1.2000000000000002 instead of 1.2. Without the fix, that number
|
|
;; survives through add-typography unchanged, and (ctt/check-typography)
|
|
;; would throw because :line-height must be a :string.
|
|
;; With the fix (mth/precision + str), it is normalised to "1.2" before the
|
|
;; schema check runs.
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest add-typography-normalises-float-line-height
|
|
(t/async
|
|
done
|
|
(let [;; Exact value reproduced from the issue: 1.3 - 0.1 in JS
|
|
float-lh 1.2000000000000002
|
|
content (txt/change-text nil "hello" :line-height float-lh)
|
|
file (-> (cthf/sample-file :file1)
|
|
(cths/add-sample-shape :text1
|
|
:type :text
|
|
:x 0 :y 0
|
|
:content content))
|
|
shape-id (:id (cths/get-shape file :text1))
|
|
file-id (:id file)
|
|
store (ths/setup-store file)
|
|
events [;; Pre-select the text shape so add-typography can find it
|
|
(fn [state] (assoc-in state [:workspace-local :selected] #{shape-id}))
|
|
(dwte/add-typography file-id)]]
|
|
|
|
(ths/run-store
|
|
store done events
|
|
(fn [new-state]
|
|
(let [file' (ths/get-file-from-state new-state)
|
|
typographies (vals (get-in file' [:data :typographies]))]
|
|
(t/is (= 1 (count typographies))
|
|
"exactly one typography was added")
|
|
(t/is (= "1.2" (:line-height (first typographies)))
|
|
"float line-height is normalised to 2-decimal string")))))))
|
|
|
|
(t/deftest add-typography-truncates-line-height-to-two-decimals
|
|
(t/async
|
|
done
|
|
(let [;; A value with more than 2 decimal places: 1.234234234 → "1.23"
|
|
long-lh 1.234234234
|
|
content (txt/change-text nil "hello" :line-height long-lh)
|
|
file (-> (cthf/sample-file :file1)
|
|
(cths/add-sample-shape :text1
|
|
:type :text
|
|
:x 0 :y 0
|
|
:content content))
|
|
shape-id (:id (cths/get-shape file :text1))
|
|
file-id (:id file)
|
|
store (ths/setup-store file)
|
|
events [(fn [state] (assoc-in state [:workspace-local :selected] #{shape-id}))
|
|
(dwte/add-typography file-id)]]
|
|
|
|
(ths/run-store
|
|
store done events
|
|
(fn [new-state]
|
|
(let [file' (ths/get-file-from-state new-state)
|
|
typographies (vals (get-in file' [:data :typographies]))]
|
|
(t/is (= 1 (count typographies))
|
|
"exactly one typography was added")
|
|
(t/is (= "1.23" (:line-height (first typographies)))
|
|
"line-height with more than 2 decimals is truncated to 2")))))))
|
|
|
|
(t/deftest add-typography-normalises-float-letter-spacing
|
|
(t/async
|
|
done
|
|
(let [;; Analogous imprecision for letter-spacing
|
|
float-ls 0.10000000000000001
|
|
content (txt/change-text nil "hello" :letter-spacing float-ls)
|
|
file (-> (cthf/sample-file :file1)
|
|
(cths/add-sample-shape :text1
|
|
:type :text
|
|
:x 0 :y 0
|
|
:content content))
|
|
shape-id (:id (cths/get-shape file :text1))
|
|
file-id (:id file)
|
|
store (ths/setup-store file)
|
|
events [(fn [state] (assoc-in state [:workspace-local :selected] #{shape-id}))
|
|
(dwte/add-typography file-id)]]
|
|
|
|
(ths/run-store
|
|
store done events
|
|
(fn [new-state]
|
|
(let [file' (ths/get-file-from-state new-state)
|
|
typographies (vals (get-in file' [:data :typographies]))]
|
|
(t/is (= 1 (count typographies))
|
|
"exactly one typography was added")
|
|
(t/is (= "0.1" (:letter-spacing (first typographies)))
|
|
"float letter-spacing is normalised to 2-decimal string")))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: add-typography must not create an asset from a deleted font
|
|
;;
|
|
;; A text shape can keep referencing a font-id that is no longer installed
|
|
;; (the font was removed, or belongs to an unavailable library). Creating a
|
|
;; typography from it would bake that broken font-id into a brand-new asset.
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest add-typography-skips-shape-with-deleted-font
|
|
(t/async
|
|
done
|
|
(let [content (txt/change-text nil "hello" :font-id "deleted-font-id")
|
|
file (-> (cthf/sample-file :file1)
|
|
(cths/add-sample-shape :text1
|
|
:type :text
|
|
:x 0 :y 0
|
|
:content content))
|
|
shape-id (:id (cths/get-shape file :text1))
|
|
file-id (:id file)
|
|
store (ths/setup-store file)
|
|
events [(fn [state] (assoc-in state [:workspace-local :selected] #{shape-id}))
|
|
(dwte/add-typography file-id)]]
|
|
|
|
(ths/run-store
|
|
store done events
|
|
(fn [new-state]
|
|
(let [file' (ths/get-file-from-state new-state)
|
|
typographies (vals (get-in file' [:data :typographies]))
|
|
shape' (cths/get-shape file' :text1)]
|
|
(t/is (= 0 (count typographies))
|
|
"no typography was added for a shape with a deleted font")
|
|
(t/is (nil? (:typography-ref-id shape'))
|
|
"the shape was not linked to a typography")))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: add-typography must not create an asset from two or more selected texts
|
|
;;
|
|
;; Deriving a typography from a specific text only makes sense for a single
|
|
;; shape; with several texts selected there is no one style to capture.
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest add-typography-skips-multiple-selected-texts
|
|
(t/async
|
|
done
|
|
(let [file (-> (cthf/sample-file :file1)
|
|
(cths/add-sample-shape :text1
|
|
:type :text
|
|
:x 0 :y 0
|
|
:content (txt/change-text nil "hello"))
|
|
(cths/add-sample-shape :text2
|
|
:type :text
|
|
:x 0 :y 100
|
|
:content (txt/change-text nil "world")))
|
|
shape-id-1 (:id (cths/get-shape file :text1))
|
|
shape-id-2 (:id (cths/get-shape file :text2))
|
|
file-id (:id file)
|
|
store (ths/setup-store file)
|
|
events [(fn [state] (assoc-in state [:workspace-local :selected] #{shape-id-1 shape-id-2}))
|
|
(dwte/add-typography file-id)]]
|
|
|
|
(ths/run-store
|
|
store done events
|
|
(fn [new-state]
|
|
(let [file' (ths/get-file-from-state new-state)
|
|
typographies (vals (get-in file' [:data :typographies]))]
|
|
(t/is (= 0 (count typographies))
|
|
"no typography was added when two texts are selected")))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: save-default-font must not persist typography refs into the global default font
|
|
;;
|
|
;; Root cause of #10925: typography assets are file-specific references, but
|
|
;; save-default-font used to write :typography-ref-id / :typography-ref-file into the
|
|
;; session-global [:workspace-global :default-font]. That state survives a file
|
|
;; switch, and v2-default-text-content bakes it into brand-new text shapes in
|
|
;; the other file, so they got a non-existent typography asset instead of the
|
|
;; default Penpot font. save-default-font now strips those two keys.
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest save-font-strips-typography-refs-from-default-font
|
|
(t/async
|
|
done
|
|
(let [file (-> (cthf/sample-file :file1)
|
|
(cths/add-sample-shape :text1
|
|
:type :text
|
|
:x 0 :y 0
|
|
:content (txt/change-text nil "hello")))
|
|
store (ths/setup-store file)
|
|
attrs {:font-id "roboto"
|
|
:font-family "Roboto"
|
|
:font-variant-id "regular"
|
|
:font-size "14"
|
|
:typography-ref-id (uuid/next)
|
|
:typography-ref-file (:id file)}]
|
|
(ths/run-store
|
|
store done [(dwt/save-default-font attrs)]
|
|
(fn [new-state]
|
|
(let [default-font (get-in new-state [:workspace-global :default-font])]
|
|
(t/is (some? default-font))
|
|
(t/is (= "roboto" (:font-id default-font)))
|
|
(t/is (nil? (:typography-ref-id default-font)))
|
|
(t/is (nil? (:typography-ref-file default-font)))))))))
|
|
|
|
(t/deftest save-font-preserves-other-font-attrs
|
|
(t/async
|
|
done
|
|
(let [store (ths/setup-store (cthf/sample-file :file1))
|
|
attrs {:font-family "Open Sans"
|
|
:font-id "opensans"
|
|
:font-variant-id "regular"
|
|
:font-size "18"
|
|
:line-height "1.5"
|
|
:letter-spacing "0"
|
|
:typography-ref-id (uuid/next)
|
|
:typography-ref-file (uuid/next)}]
|
|
(ths/run-store store done [(dwt/save-default-font attrs)]
|
|
(fn [new-state]
|
|
(let [default-font (get-in new-state [:workspace-global :default-font])]
|
|
(t/is (= "Open Sans" (:font-family default-font)))
|
|
(t/is (= "18" (:font-size default-font)))
|
|
(t/is (= "1.5" (:line-height default-font)))
|
|
(t/is (nil? (:typography-ref-id default-font)))
|
|
(t/is (nil? (:typography-ref-file default-font)))))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: fix-position with degenerate selrect
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest fix-position-zero-width-selrect-does-not-throw
|
|
(t/testing "fix-position on a shape with zero selrect width does not throw"
|
|
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 50)
|
|
modifiers (ctm/change-dimensions-modifiers shape :width 200 {:ignore-lock? true})
|
|
shape' (assoc shape :modifiers modifiers)
|
|
result (vth/fix-position shape')]
|
|
(t/is (some? result))
|
|
(t/is (some? (:selrect result))))))
|
|
|
|
(t/deftest fix-position-zero-height-selrect-does-not-throw
|
|
(t/testing "fix-position on a shape with zero selrect height does not throw"
|
|
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 100 :height 0)
|
|
modifiers (ctm/change-dimensions-modifiers shape :height 80 {:ignore-lock? true})
|
|
shape' (assoc shape :modifiers modifiers)
|
|
result (vth/fix-position shape')]
|
|
(t/is (some? result))
|
|
(t/is (some? (:selrect result))))))
|
|
|
|
(t/deftest fix-position-zero-width-and-height-selrect-does-not-throw
|
|
(t/testing "fix-position on a fully degenerate selrect does not throw"
|
|
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
|
modifiers (ctm/change-dimensions-modifiers shape :width 150 {:ignore-lock? true})
|
|
shape' (assoc shape :modifiers modifiers)
|
|
result (vth/fix-position shape')]
|
|
(t/is (some? result))
|
|
(t/is (some? (:selrect result))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests: ensure-valid-text-content
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest ensure-valid-text-content-empty-children-repaired
|
|
(t/testing "root with empty :children vector is repaired to canonical tree"
|
|
(let [broken {:type "root"
|
|
:vertical-align "top"
|
|
:children []}
|
|
fixed (dwt/ensure-valid-text-content broken)]
|
|
(t/is (= "root" (:type fixed)))
|
|
(t/is (vector? (:children fixed)))
|
|
(t/is (= 1 (count (:children fixed)))
|
|
"exactly one paragraph-set is seeded")
|
|
(t/is (= "paragraph-set" (get-in fixed [:children 0 :type])))
|
|
(t/is (pos? (count (get-in fixed [:children 0 :children])))
|
|
"paragraph-set has at least one paragraph")
|
|
(t/is (= "" (get-in fixed [:children 0 :children 0 :children 0 :text]))
|
|
"seeded span has empty text")
|
|
(t/is (= "top" (:vertical-align fixed))
|
|
"preserves the original :vertical-align"))))
|
|
|
|
(t/deftest ensure-valid-text-content-missing-children-repaired
|
|
(t/testing "root with no :children key is repaired to canonical tree"
|
|
(let [broken {:type "root" :vertical-align "center"}
|
|
fixed (dwt/ensure-valid-text-content broken)]
|
|
(t/is (vector? (:children fixed)))
|
|
(t/is (pos? (count (:children fixed))))
|
|
(t/is (= "center" (:vertical-align fixed))))))
|
|
|
|
(t/deftest ensure-valid-text-content-healthy-tree-unchanged
|
|
(t/testing "a well-formed content is returned unchanged"
|
|
(let [healthy {:type "root"
|
|
:children [{:type "paragraph-set"
|
|
:children [{:type "paragraph"
|
|
:children [{:text "hello"}]}]}]}
|
|
fixed (dwt/ensure-valid-text-content healthy)]
|
|
(t/is (= healthy fixed)))))
|
|
|
|
(t/deftest ensure-valid-text-content-nil-unchanged
|
|
(t/testing "nil content is returned unchanged (no repair)"
|
|
(t/is (nil? (dwt/ensure-valid-text-content nil)))))
|
|
|
|
(t/deftest ensure-valid-text-content-non-root-unchanged
|
|
(t/testing "a non-root content (e.g. paragraph) is left alone"
|
|
(let [node {:type "paragraph" :children []}]
|
|
(t/is (= node (dwt/ensure-valid-text-content node))))))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; txt/rtl-content?
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defn- make-content
|
|
"Text content whose paragraphs carry the given `:text-direction` values; a
|
|
`nil` entry leaves the attribute out entirely."
|
|
[& directions]
|
|
{:type "root"
|
|
:children [{:type "paragraph-set"
|
|
:children (vec (for [direction directions]
|
|
(cond-> {:type "paragraph"
|
|
:children [{:text "hello"}]}
|
|
(some? direction)
|
|
(assoc :text-direction direction))))}]})
|
|
|
|
(t/deftest rtl-content-single-rtl-paragraph
|
|
(t/testing "a lone rtl paragraph makes the content rtl"
|
|
(t/is (true? (txt/rtl-content? (make-content "rtl"))))))
|
|
|
|
(t/deftest rtl-content-every-paragraph-rtl
|
|
(t/testing "several paragraphs, all rtl"
|
|
(t/is (true? (txt/rtl-content? (make-content "rtl" "rtl" "rtl"))))))
|
|
|
|
(t/deftest rtl-content-mixed-directions
|
|
(t/testing "a mix of rtl and ltr is not rtl"
|
|
(t/is (false? (txt/rtl-content? (make-content "rtl" "ltr"))))))
|
|
|
|
(t/deftest rtl-content-missing-direction-on-one-paragraph
|
|
(t/testing "a paragraph without :text-direction defaults to ltr, so the
|
|
content is not rtl"
|
|
(t/is (false? (txt/rtl-content? (make-content "rtl" nil))))))
|
|
|
|
(t/deftest rtl-content-all-ltr
|
|
(t/testing "all-ltr content is not rtl"
|
|
(t/is (false? (txt/rtl-content? (make-content "ltr" "ltr"))))))
|
|
|
|
(t/deftest rtl-content-none-direction-is-ltr
|
|
(t/testing "\"none\" (the sidebar's un-toggled value) is ltr, matching what
|
|
translate-text-direction sends to the renderer"
|
|
(t/is (false? (txt/rtl-content? (make-content "none"))))))
|
|
|
|
(t/deftest rtl-content-without-paragraphs
|
|
(t/testing "a root with no paragraph nodes is not rtl"
|
|
(t/is (false? (txt/rtl-content? {:type "root" :children []})))))
|
|
|
|
(t/deftest rtl-content-nil
|
|
(t/testing "nil content is not rtl and does not throw"
|
|
(t/is (false? (txt/rtl-content? nil)))))
|