mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
✨ Test what copy texts keep through sync and switches
Add a battery of tests for text overrides in copies. - A short notation for text content in tests (thtx): paragraphs of runs with short style names, read back with equal runs joined, edits in the notation, set-text and set-content through the change path, and the override rule for texts (override-units), which says which overrides a copy must hold against its main. - Sync matrix: every whole-text copy edit against every main edit, plus the cards of the worked examples, the "Hello world" table, a token on a copy, typographies and a library color sync. Where the code differs from decisions (k) to (o), the result is pinned against F72 to F80. - Switch matrix: copy edits against variants that differ in letters, size, color, a new line or a bolded word; F76, F77, F79 and F81 pinned. - Composable case BA: EditText also edits fills, font family and a whole text; the copy and the main change different styles, then reset or update main, in both runners; F72 pinned. - Editor output: the content the WASM and v1 editors commit when a copy text is typed into, retyped, split with Enter and has a word bolded, recorded in the workspace and replayed on a copy. Typing sets only the text groups and a style change no text override; F79 pinned on both editors' content.
This commit is contained in:
parent
270d0ac35d
commit
85f663d000
@ -160,9 +160,13 @@ Two pure runners (no store, JVM + CLJS):
|
||||
- Case assertions a known bug breaks use `tm/known-failure bug expected pinned actual`: a
|
||||
strict expected failure at assertion level (passes only while `actual` is the pinned wrong
|
||||
value). The undo-check marks cover only round-trip failures.
|
||||
- Text: `n/edit-text` (`:text`/`:font-size` of the first span) with `n/text-edit-commit`, which
|
||||
includes the editor's automatic rename when the name was generated from the old content;
|
||||
setup `simple-text-component-with-copy` (text named after its content).
|
||||
- Text: `n/edit-text` with `n/text-edit-commit`, which includes the editor's automatic rename
|
||||
when the name was generated from the old content. Edits: `:text`/`:font-size` of the first
|
||||
span, `:fills`/`:font-family` of all of the text, `:content` (the whole text in the
|
||||
notation of `app.common.test-helpers.texts`, alias `thtx`; read back with
|
||||
`(n/text-span-value shape :content)`). Setups `simple-text-component-with-copy` (text named
|
||||
after its content) and `two-style-text-component-with-copy` ("Price: " + bold "$10",
|
||||
written as the editors write it).
|
||||
- The interpreter unwraps nested choices (`(tm/optional (tm/one-of …))`); `(optional
|
||||
(in-sequence …))` is still not flattened.
|
||||
- Depth sweeps: `depth-sweep` in `cases.cljc`, a one-of over skip,
|
||||
@ -203,7 +207,7 @@ Two pure runners (no store, JVM + CLJS):
|
||||
interpreter): the path is cleaned and a main root's component is renamed too. Case letters B..Z, then AA..;
|
||||
the sweeps (K: depth × edit-precedence; L: swaps; M: variant switches; N: rotated-instance
|
||||
geometry, on the #10109 fix branch until merged; O: switch of an overridden copy, mains
|
||||
agreeing or not; Q: detach at depth 0..2, nested copies stay linked; R: update-main at depth 0..2 with two copies; S: delete/restore with a copy on another page; T: duplicate a copy at depth 0..2; U: move a copy into a main with a copy; V: new edit after undo empties redo; W: `(n/undo 3)` over nested cascades; X: undo before the edit's sync, frontend only; AJ: F43, one undo after a nested sync reverts only the reflow, frontend only; Y: text override × main text change, per subgroup; Z: main child move, frontend only; AA..AE: swap outcomes asserted on values, not slot storage, so they survive the Phase 2b swap model; AF: grid cell move vs a swapped copy child, setup `grid-component-with-copy`; AG/AH/AI: ports of the plugin's slot-alignment cases, setup `component-with-sibling-instances`, check `setup/misaligned-sub-heads`; AK: update main into a grid main, F49 pinned; AL: update main renames the component (`change-property` takes `:name`); AM: update main of a swap inside a group, setup `grouped-component-with-copy`, F16 pinned; AN: flex child margin override through main edit, reset and update main, setup `flex-component-with-copy`; `change-property` takes `:margin`, which the interpreter sends as `dwsl/update-layout-child`; AO: a copy dropped into a grid cell of a main (`nest-copy` with `:cell`, sent as `dwt/move-shapes-to-frame`), F41 pinned; AP: a library main's middle child removal reaches a copy in another file (in one file, deleting a main child deletes the copies' children itself, so sync never meets the copy child), setup `cross-file-component-with-many-children`; AQ: main change reaches a copy nested in a sub-board, setup `component-nested-in-a-sub-board`; AR: variant property edits keep a member's copies as they are, then a switch finds its member by the edited properties; AS: an added variant joins the set and is a switch target; AT..AX, frontend only: transform into a variant, combine, rename a variant (F6 pinned: the transaction keeps the container commit's undo group, the copies' sync takes the main renames'), rename in layers + remove empty properties + member error, the shortcuts that add members; AY: a border radius token on a main reaches untouched copies and its removal too, F55 pinned; AZ: a nested copy pasted out of its main keeps the main's overrides) are the flagship pattern — read them before
|
||||
agreeing or not; Q: detach at depth 0..2, nested copies stay linked; R: update-main at depth 0..2 with two copies; S: delete/restore with a copy on another page; T: duplicate a copy at depth 0..2; U: move a copy into a main with a copy; V: new edit after undo empties redo; W: `(n/undo 3)` over nested cascades; X: undo before the edit's sync, frontend only; AJ: F43, one undo after a nested sync reverts only the reflow, frontend only; Y: text override × main text change, per subgroup; Z: main child move, frontend only; AA..AE: swap outcomes asserted on values, not slot storage, so they survive the Phase 2b swap model; AF: grid cell move vs a swapped copy child, setup `grid-component-with-copy`; AG/AH/AI: ports of the plugin's slot-alignment cases, setup `component-with-sibling-instances`, check `setup/misaligned-sub-heads`; AK: update main into a grid main, F49 pinned; AL: update main renames the component (`change-property` takes `:name`); AM: update main of a swap inside a group, setup `grouped-component-with-copy`, F16 pinned; AN: flex child margin override through main edit, reset and update main, setup `flex-component-with-copy`; `change-property` takes `:margin`, which the interpreter sends as `dwsl/update-layout-child`; AO: a copy dropped into a grid cell of a main (`nest-copy` with `:cell`, sent as `dwt/move-shapes-to-frame`), F41 pinned; AP: a library main's middle child removal reaches a copy in another file (in one file, deleting a main child deletes the copies' children itself, so sync never meets the copy child), setup `cross-file-component-with-many-children`; AQ: main change reaches a copy nested in a sub-board, setup `component-nested-in-a-sub-board`; AR: variant property edits keep a member's copies as they are, then a switch finds its member by the edited properties; AS: an added variant joins the set and is a switch target; AT..AX, frontend only: transform into a variant, combine, rename a variant (F6 pinned: the transaction keeps the container commit's undo group, the copies' sync takes the main renames'), rename in layers + remove empty properties + member error, the shortcuts that add members; AY: a border radius token on a main reaches untouched copies and its removal too, F55 pinned; AZ: a nested copy pasted out of its main keeps the main's overrides; BA: a copy text's style override against a main style change, then reset or update main, F72 pinned) are the flagship pattern — read them before
|
||||
writing a new sweep.
|
||||
|
||||
**Scenario lineage model** (behind the sweeps): scenario ops track named component lineages as
|
||||
|
||||
@ -80,6 +80,7 @@
|
||||
[app.common.test-helpers.composable.comp.setups :as setup]
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.texts :as thtx]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
@ -694,6 +695,70 @@
|
||||
(t/is (= #{:content-group (subgroup (:edit edited))}
|
||||
(:touched copy))))))}))
|
||||
|
||||
(defn- text-restyle
|
||||
"The restyle of a text in the notation that the `:fills` or `:font-family`
|
||||
text edit `node` makes."
|
||||
[node]
|
||||
(let [{:keys [edit value]} node]
|
||||
(case edit
|
||||
:fills #(thtx/restyle % :color (some (fn [[k v]] (when (= v (:fill-color (first value))) k))
|
||||
thtx/colors))
|
||||
:font-family #(thtx/restyle % :family value))))
|
||||
|
||||
(defn text-style-override-against-main-style-change
|
||||
"Case BA. A main text in two styles (\"Price: \" and a bold \"$10\") and
|
||||
its copy. The copy changes the fill or the font family of all of its text,
|
||||
then the main changes one of them and syncs, then optionally the copy is
|
||||
reset or updates the main. Each style is an override of its own (decision
|
||||
(k)): after the sync the copy keeps its style and takes the main's other
|
||||
one. A reset makes the copy equal to the main and clears its overrides.
|
||||
An update main gives the main the copy's style, keeps the main's other
|
||||
one, and clears the copy's overrides."
|
||||
[]
|
||||
(let [copy-edit (tm/one-of [(n/edit-text :copy-instance :fills [{:fill-color "#FF0000" :fill-opacity 1}])
|
||||
(n/edit-text :copy-instance :font-family "Roboto")])
|
||||
main-edit (tm/one-of [(n/edit-text :main-instance :fills [{:fill-color "#0000FF" :fill-opacity 1}])
|
||||
(n/edit-text :main-instance :font-family "Inter")])
|
||||
reset (n/reset :copy-root)
|
||||
push (n/update-main :copy-root)]
|
||||
{:setup setup/two-style-text-component-with-copy
|
||||
:operation (tm/in-sequence [copy-edit main-edit (tm/optional (tm/one-of [reset push]))])
|
||||
:asserter (fn [s]
|
||||
(let [copy-node (tm/get-choice s copy-edit)
|
||||
main-node (tm/get-choice s main-edit)
|
||||
same? (= (:edit copy-node) (:edit main-node))
|
||||
main-text ((text-restyle main-node) setup/two-style-text)
|
||||
merged ((text-restyle copy-node) main-text)
|
||||
as-it-was ((text-restyle copy-node) setup/two-style-text)
|
||||
copy (setup/copy-instance s)
|
||||
main (setup/main-instance s)
|
||||
text-of #(n/text-span-value % :content)]
|
||||
(cond
|
||||
(tm/applied? s reset)
|
||||
(do
|
||||
(t/is (= (thtx/normalize main-text) (text-of copy)))
|
||||
(t/is (= (thtx/normalize main-text) (text-of main)))
|
||||
(t/is (empty? (:touched copy))))
|
||||
|
||||
(tm/applied? s push)
|
||||
(do
|
||||
(if same?
|
||||
(t/is (= (thtx/normalize merged) (text-of main)))
|
||||
;; the copy pushes all of its styles, since all
|
||||
;; of them stayed as the copy left them
|
||||
(tm/known-failure "F72" (thtx/normalize merged) (thtx/normalize as-it-was)
|
||||
(text-of main)))
|
||||
(t/is (= (text-of main) (text-of copy)))
|
||||
(t/is (empty? (:touched copy))))
|
||||
|
||||
:else
|
||||
(do
|
||||
(t/is (= (thtx/normalize main-text) (text-of main)))
|
||||
(if same?
|
||||
(t/is (= (thtx/normalize merged) (text-of copy)))
|
||||
(tm/known-failure "F72" (thtx/normalize merged) (thtx/normalize as-it-was)
|
||||
(text-of copy)))))))}))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Swap outcomes
|
||||
;;
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.test-helpers.texts :as thtx]
|
||||
[app.common.test-helpers.tokens :as tht]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
@ -168,32 +169,62 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; edit-text — edit the content of a text shape
|
||||
;;
|
||||
;; An edit of the characters (`:text`) or font size (`:font-size`) of a text's
|
||||
;; first span, as the text editor commits it when the edit ends: a new
|
||||
;; An edit of a text as the text editor commits it when the edit ends: a new
|
||||
;; `:content`, and a new name when the text still has the name generated from
|
||||
;; its old content. The frontend interpreter commits it through
|
||||
;; its old content. The edit is one of
|
||||
;; :text the characters of the first span;
|
||||
;; :font-size the font size of the first span;
|
||||
;; :fills the fills of all of the text (select all, then a color);
|
||||
;; :font-family the font family of all of the text;
|
||||
;; :content the whole text, written in the notation of
|
||||
;; `app.common.test-helpers.texts`.
|
||||
;; The frontend interpreter commits it through
|
||||
;; `v2-update-text-shape-content`, as the editor does on blur.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(def ^:private first-span [:content :children 0 :children 0 :children 0])
|
||||
|
||||
(def ^:private text-edits #{:text :font-size :fills :font-family :content})
|
||||
|
||||
(defn- generated-name
|
||||
"The name the text editor gives a text after its `content`."
|
||||
[content]
|
||||
(txt/generate-shape-name (txt/content->text content)))
|
||||
|
||||
(defn- restyle-all
|
||||
[content attrs]
|
||||
(txt/transform-nodes (some-fn txt/is-text-node? txt/is-paragraph-node?) #(merge % attrs) content))
|
||||
|
||||
(defn text-edit-commit
|
||||
"What the text editor commits when the `edit` (`:text` or `:font-size`) of
|
||||
the first span of `shape` becomes `value`: `:content`, and `:name` when the
|
||||
editor renames the shape after its text, which it does when the shape still
|
||||
has the name generated from its old content. Public so the frontend
|
||||
interpreter commits the same edit."
|
||||
"What the text editor commits when the `edit` (see `text-edits`) of
|
||||
`shape` becomes `value`: `:content`, and `:name` when the editor renames
|
||||
the shape after its text, which it does when the shape still has the name
|
||||
generated from its old content. Public so the frontend interpreter commits
|
||||
the same edit."
|
||||
[shape edit value]
|
||||
(when-not (contains? #{:text :font-size} edit)
|
||||
(when-not (contains? text-edits edit)
|
||||
(throw (ex-info (str "text-edit-commit: unsupported edit " (pr-str edit)
|
||||
" (supported: :text, :font-size)")
|
||||
" (supported: " (pr-str text-edits) ")")
|
||||
{:edit edit})))
|
||||
(let [content (assoc-in (:content shape) (conj (subvec first-span 1) edit) value)]
|
||||
(let [content (case edit
|
||||
(:text :font-size)
|
||||
(assoc-in (:content shape) (conj (subvec first-span 1) edit) value)
|
||||
|
||||
:fills
|
||||
(restyle-all (:content shape) {:fills value})
|
||||
|
||||
:font-family
|
||||
(txt/transform-nodes (some-fn txt/is-text-node? txt/is-paragraph-node?)
|
||||
#(assoc %
|
||||
:font-family value
|
||||
:font-id (get thtx/font-ids value)
|
||||
:font-variant-id (thtx/variant-id value
|
||||
(d/parse-integer (:font-weight %))
|
||||
(= "italic" (:font-style %))))
|
||||
(:content shape))
|
||||
|
||||
:content
|
||||
(thtx/content value))]
|
||||
(cond-> {:content content}
|
||||
(= (:name shape) (generated-name (:content shape)))
|
||||
(assoc :name (generated-name content)))))
|
||||
@ -204,9 +235,12 @@
|
||||
(merge shape (text-edit-commit shape edit value)))
|
||||
|
||||
(defn text-span-value
|
||||
"The `edit` (`:text` or `:font-size`) of `shape`'s first span."
|
||||
"The `edit` (see `text-edits`) of `shape`: of its first span, or for
|
||||
`:content` its whole text in the notation of `app.common.test-helpers.texts`."
|
||||
[shape edit]
|
||||
(get-in shape (conj first-span edit)))
|
||||
(if (= edit :content)
|
||||
(thtx/view (:content shape))
|
||||
(get-in shape (conj first-span edit))))
|
||||
|
||||
(defrecord EditText [target edit value]
|
||||
IComponentOperation
|
||||
@ -229,9 +263,9 @@
|
||||
(tm/record-application this {:target target :edit edit :value value})))))
|
||||
|
||||
(defn edit-text
|
||||
"Constructor for the text edit operation: set the `edit` (`:text` or
|
||||
`:font-size`) of the first span of the text bound to `target` to `value`.
|
||||
On the frontend it dispatches the real `v2-update-text-shape-content`."
|
||||
"Constructor for the text edit operation: set the `edit` (see
|
||||
`text-edits`) of the text bound to `target` to `value`. On the frontend it
|
||||
dispatches the real `v2-update-text-shape-content`."
|
||||
[target edit value]
|
||||
(tm/assign-id (->EditText target edit value)))
|
||||
|
||||
@ -345,9 +379,11 @@
|
||||
(extend-type EditText
|
||||
IPropertyCheck
|
||||
(applied-property [node] (:edit node))
|
||||
(applied-value [node] (:value node))
|
||||
(applied-value [node]
|
||||
(cond-> (:value node)
|
||||
(= :content (:edit node)) (thtx/normalize)))
|
||||
(has-property-of [node shape]
|
||||
(= (:value node) (text-span-value shape (:edit node)))))
|
||||
(= (applied-value node) (text-span-value shape (:edit node)))))
|
||||
|
||||
;; ===========================================================================
|
||||
;; Scenario building blocks
|
||||
|
||||
@ -19,7 +19,8 @@
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]))
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.test-helpers.texts :as thtx]))
|
||||
|
||||
;; The fill the labeled setups give the main child.
|
||||
(def labeled-fill "#abcdef")
|
||||
@ -122,6 +123,28 @@
|
||||
:main-root :main-root
|
||||
:copy-root :copy-root})))
|
||||
|
||||
(def two-style-text
|
||||
"The text of `two-style-text-component-with-copy`, in the notation of
|
||||
`app.common.test-helpers.texts`."
|
||||
[[["Price: "] ["$10" {:weight 700}]]])
|
||||
|
||||
(defn two-style-text-component-with-copy
|
||||
"As `simple-text-component-with-copy`, with the text \"Price: $10\" in two
|
||||
styles (`two-style-text`), written as the editors write it."
|
||||
[]
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame-with-text :main-root :main-text "Price: $10"
|
||||
:frame-params {:width 100 :height 100}
|
||||
:child-params {:name "Price: $10"
|
||||
:content (thtx/content two-style-text)})
|
||||
(thc/make-component :component1 :main-root)
|
||||
(thc/instantiate-component :component1 :copy-root
|
||||
:children-labels [:copy-text]))]
|
||||
(tm/make-situation file {:main-instance :main-text
|
||||
:copy-instance :copy-text
|
||||
:main-root :main-root
|
||||
:copy-root :copy-root})))
|
||||
|
||||
(defn- label-copy-children!
|
||||
"Register each label of `labels` (main child label -> copy child label) on
|
||||
the child of the copy `copy-label` that references that main child."
|
||||
|
||||
542
common/src/app/common/test_helpers/texts.cljc
Normal file
542
common/src/app/common/test_helpers/texts.cljc
Normal file
@ -0,0 +1,542 @@
|
||||
;; 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 app.common.test-helpers.texts
|
||||
"Text content in a short notation, for tests of text overrides in copies.
|
||||
|
||||
A text is a vector of paragraphs, optionally led by a map of root styles
|
||||
(`{:valign \"bottom\"}`). A paragraph is a vector of runs, optionally led
|
||||
by a map of paragraph styles (`:align`, `:direction`, `:line-height`). A
|
||||
run is `[letters]` or `[letters styles]`, where styles use the short names
|
||||
below and list only what differs from `base-style`:
|
||||
|
||||
[[[\"Price: \"] [\"$10\" {:weight 700}]]]
|
||||
[[{:align \"center\"} [\"Free shipping\" {:size 18}]]]
|
||||
|
||||
Run styles: `:family` (a key of `font-ids`), `:weight`, `:italic`, `:size`,
|
||||
`:spacing` (letter spacing), `:case` (text transform), `:decoration`,
|
||||
`:color` (a key of `colors`) and `:typography` (the label of a typography
|
||||
of the file `:file1`). Any text attribute can also be given by its own
|
||||
key, for values the short names cannot say.
|
||||
|
||||
`content` builds what the DOM editor writes (`from_dom`): every run
|
||||
carries every run style, fills included, and every paragraph carries its
|
||||
paragraph styles plus the styles of its first run. What the WASM and v1
|
||||
editors write is in `text-editor-output-test`. `view` reads content back into the
|
||||
notation and joins adjacent runs with equal styles, so tests compare what
|
||||
a text shows, not how it is split into nodes."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.types.text :as txt]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(def font-ids
|
||||
"Font family name -> font id, for the families the notation knows."
|
||||
{"sourcesanspro" "sourcesanspro"
|
||||
"Inter" "gfont-inter"
|
||||
"Roboto" "gfont-roboto"})
|
||||
|
||||
(def colors
|
||||
{:black "#000000"
|
||||
:red "#FF0000"
|
||||
:blue "#0000FF"
|
||||
:purple "#7B3FD1"})
|
||||
|
||||
(def base-style
|
||||
"The run styles of a text with no styles given."
|
||||
{:font-id "sourcesanspro"
|
||||
:font-family "sourcesanspro"
|
||||
:font-variant-id "regular"
|
||||
:font-size "14"
|
||||
:font-weight "400"
|
||||
:font-style "normal"
|
||||
:letter-spacing "0"
|
||||
:text-transform "none"
|
||||
:text-decoration "none"
|
||||
:fills [{:fill-color "#000000" :fill-opacity 1}]})
|
||||
|
||||
(def base-paragraph-style
|
||||
{:text-align "left"
|
||||
:text-direction "ltr"
|
||||
:line-height "1.2"})
|
||||
|
||||
(def base-root-style
|
||||
{:vertical-align "top"})
|
||||
|
||||
(def ^:private named-weights
|
||||
"Weight -> the name the variant ids of `sourcesanspro` give it."
|
||||
{400 "regular" 700 "bold" 900 "black"})
|
||||
|
||||
(defn variant-id
|
||||
"The id of the variant of `family` with `weight` (a number), italic or not."
|
||||
[family weight italic?]
|
||||
(let [weight (if (or (= weight 400) (= family "sourcesanspro"))
|
||||
(get named-weights weight (str weight))
|
||||
(str weight))]
|
||||
(cond
|
||||
(and (= weight "regular") italic?) "italic"
|
||||
italic? (str weight "italic")
|
||||
:else weight)))
|
||||
|
||||
(defn- ->number
|
||||
[s]
|
||||
(cond
|
||||
(number? s) s
|
||||
(re-matches #"-?\d+" s) (d/parse-integer s)
|
||||
:else (d/parse-double s)))
|
||||
|
||||
(defn- raw-attrs
|
||||
"The keys of `style` that are text attributes given by their own name.
|
||||
`:line-height` is a short name too, and is read as one."
|
||||
[style]
|
||||
(select-keys style (disj txt/text-all-attrs :line-height)))
|
||||
|
||||
(defn- run-attrs
|
||||
[{:keys [family weight italic size spacing case decoration color typography] :as style}]
|
||||
(let [weight (or weight 400)]
|
||||
(cond-> (assoc base-style
|
||||
:font-weight (str weight)
|
||||
:font-style (if italic "italic" "normal")
|
||||
:font-variant-id (variant-id (or family "sourcesanspro") weight italic))
|
||||
family (assoc :font-family family :font-id (get font-ids family))
|
||||
size (assoc :font-size (str size))
|
||||
spacing (assoc :letter-spacing (str spacing))
|
||||
case (assoc :text-transform case)
|
||||
decoration (assoc :text-decoration decoration)
|
||||
color (assoc :fills [{:fill-color (get colors color) :fill-opacity 1}])
|
||||
typography (assoc :typography-ref-id (thi/id typography)
|
||||
:typography-ref-file (thi/id :file1))
|
||||
:always (merge (raw-attrs style)))))
|
||||
|
||||
(defn- paragraph-attrs
|
||||
[{:keys [align direction line-height] :as style}]
|
||||
(cond-> base-paragraph-style
|
||||
align (assoc :text-align align)
|
||||
direction (assoc :text-direction direction)
|
||||
line-height (assoc :line-height (str line-height))
|
||||
:always (merge (raw-attrs style))))
|
||||
|
||||
(defn- split-styles
|
||||
"Split a vector into its leading styles map (or nil) and the rest."
|
||||
[v]
|
||||
(if (map? (first v))
|
||||
[(first v) (rest v)]
|
||||
[nil v]))
|
||||
|
||||
(defn content
|
||||
"Build the text content written as `text` in the notation."
|
||||
[text]
|
||||
(let [[root-style paragraphs] (split-styles text)
|
||||
paragraph (fn [p]
|
||||
(let [[style runs] (split-styles p)
|
||||
spans (mapv (fn [[letters style]]
|
||||
(assoc (run-attrs style) :text letters))
|
||||
runs)]
|
||||
(merge (dissoc (first spans) :text)
|
||||
(paragraph-attrs style)
|
||||
{:type "paragraph" :children spans})))]
|
||||
{:type "root"
|
||||
:vertical-align (:valign root-style "top")
|
||||
:children [{:type "paragraph-set"
|
||||
:children (mapv paragraph paragraphs)}]}))
|
||||
|
||||
(defn- style-diff
|
||||
"The short-name styles of `node` that differ from `base-style`. A value the
|
||||
short names cannot say shows under its own attribute."
|
||||
[node]
|
||||
(let [{:keys [font-id font-family font-variant-id font-size font-weight font-style
|
||||
letter-spacing text-transform text-decoration fills
|
||||
typography-ref-id typography-ref-file]} node
|
||||
weight (->number font-weight)
|
||||
italic? (= font-style "italic")
|
||||
color (when (and (= 1 (count fills))
|
||||
(= 1 (:fill-opacity (first fills)))
|
||||
(= #{:fill-color :fill-opacity} (set (keys (first fills)))))
|
||||
(some (fn [[k v]] (when (= v (:fill-color (first fills))) k)) colors))
|
||||
;; spans written by `txt/change-text` also carry the paragraph styles
|
||||
node (cond-> node
|
||||
(= (:text-align node) "left") (dissoc :text-align)
|
||||
(= (:text-direction node) "ltr") (dissoc :text-direction))
|
||||
known #{:text :type :key :children :line-height
|
||||
:font-id :font-family :font-variant-id :font-size :font-weight :font-style
|
||||
:letter-spacing :text-transform :text-decoration :fills
|
||||
:typography-ref-id :typography-ref-file}]
|
||||
(cond-> {}
|
||||
(not= font-family "sourcesanspro")
|
||||
(assoc :family font-family)
|
||||
|
||||
(not= font-id (get font-ids font-family))
|
||||
(assoc :font-id font-id)
|
||||
|
||||
(not= weight 400)
|
||||
(assoc :weight weight)
|
||||
|
||||
italic?
|
||||
(assoc :italic true)
|
||||
|
||||
(not (#{"normal" "italic"} font-style))
|
||||
(assoc :font-style font-style)
|
||||
|
||||
(not= font-variant-id (variant-id font-family weight italic?))
|
||||
(assoc :font-variant-id font-variant-id)
|
||||
|
||||
(not= font-size "14")
|
||||
(assoc :size (->number font-size))
|
||||
|
||||
(not= letter-spacing "0")
|
||||
(assoc :spacing (->number letter-spacing))
|
||||
|
||||
(not= text-transform "none")
|
||||
(assoc :case text-transform)
|
||||
|
||||
(not= text-decoration "none")
|
||||
(assoc :decoration text-decoration)
|
||||
|
||||
(and (some? color) (not= color :black))
|
||||
(assoc :color color)
|
||||
|
||||
(nil? color)
|
||||
(assoc :fills fills)
|
||||
|
||||
(some? typography-ref-id)
|
||||
(assoc :typography (thi/label typography-ref-id))
|
||||
|
||||
(and (some? typography-ref-id) (not= typography-ref-file (thi/id :file1)))
|
||||
(assoc :typography-ref-file typography-ref-file)
|
||||
|
||||
:always
|
||||
(merge (apply dissoc node known)))))
|
||||
|
||||
(defn- paragraph-diff
|
||||
"The paragraph styles of `paragraph` in the notation. A paragraph without
|
||||
`:line-height` (as the WASM editor writes them) takes its first run's."
|
||||
[paragraph]
|
||||
(let [{:keys [text-align text-direction]} paragraph
|
||||
line-height (or (:line-height paragraph)
|
||||
(:line-height (first (:children paragraph))))]
|
||||
(cond-> {}
|
||||
(not= text-align "left") (assoc :align text-align)
|
||||
(not= text-direction "ltr") (assoc :direction text-direction)
|
||||
(not= line-height "1.2") (assoc :line-height (->number line-height)))))
|
||||
|
||||
(defn- join-runs
|
||||
[runs]
|
||||
(reduce (fn [acc [letters style :as run]]
|
||||
(let [[prev-letters prev-style] (peek acc)]
|
||||
(if (and (some? prev-letters) (= prev-style style))
|
||||
(conj (pop acc) [(str prev-letters letters) style])
|
||||
(conj acc run))))
|
||||
[]
|
||||
runs))
|
||||
|
||||
(defn- view-run
|
||||
[[letters style]]
|
||||
(if (empty? style) [letters] [letters style]))
|
||||
|
||||
(defn view
|
||||
"Read `content` back into the notation."
|
||||
[content]
|
||||
(let [root-diff (when (not= (:vertical-align content "top") "top")
|
||||
{:valign (:vertical-align content)})
|
||||
paragraph (fn [p]
|
||||
(let [runs (->> (:children p)
|
||||
(map (fn [node] [(:text node) (style-diff node)]))
|
||||
(join-runs))
|
||||
runs (if (> (count runs) 1)
|
||||
(remove (comp empty? first) runs)
|
||||
runs)
|
||||
style (paragraph-diff p)]
|
||||
(cond->> (mapv view-run runs)
|
||||
(seq style) (into [style]))))
|
||||
paragraphs (mapv paragraph (txt/node-seq txt/is-paragraph-node? content))]
|
||||
(cond->> paragraphs
|
||||
(some? root-diff) (into [root-diff]))))
|
||||
|
||||
(defn normalize
|
||||
"`text` as `view` gives it back once built: equal styles joined, base
|
||||
styles left out."
|
||||
[text]
|
||||
(view (content text)))
|
||||
|
||||
(defn text-of
|
||||
"The text of the shape `label` in `file`, in the notation."
|
||||
[file label]
|
||||
(view (:content (ths/get-shape file label))))
|
||||
|
||||
(defn set-content
|
||||
"Replace the content of the text shape `label` with `content`, through the
|
||||
production change path, as an editor commit does."
|
||||
[file label content]
|
||||
(let [page (thf/current-page file)
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(thi/id label)}
|
||||
#(assoc % :content content)
|
||||
(:objects page)
|
||||
{})]
|
||||
(thf/apply-changes file changes)))
|
||||
|
||||
(defn set-text
|
||||
"Replace the content of the text shape `label` with `text` in the notation,
|
||||
as `set-content` does."
|
||||
[file label text]
|
||||
(set-content file label (content text)))
|
||||
|
||||
(defn add-text-component-with-copy
|
||||
"Add the component :component1, whose main :main-root holds the text
|
||||
:main-child written as `text`, and its copy :copy-root, whose text is
|
||||
:copy-child."
|
||||
[file text]
|
||||
(-> file
|
||||
(tho/add-frame-with-text :main-root :main-child ""
|
||||
:child-params {:content (content text)})
|
||||
(thc/make-component :component1 :main-root)
|
||||
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]})))
|
||||
|
||||
;; ---- Edits of a text in the notation
|
||||
|
||||
(def typography-styles
|
||||
"The short-name styles a typography sets: on runs, and `:line-height` on
|
||||
paragraphs."
|
||||
#{:family :weight :italic :size :spacing :case :line-height})
|
||||
|
||||
(defn- update-paragraphs
|
||||
"Apply `f` to each paragraph of `text` as `[styles runs]`; `f` returns the
|
||||
same pair."
|
||||
[text f]
|
||||
(let [[root-style paragraphs] (split-styles text)
|
||||
paragraphs (mapv (fn [p]
|
||||
(let [[style runs] (f (split-styles p))]
|
||||
(cond->> (vec runs)
|
||||
(seq style) (into [style]))))
|
||||
paragraphs)]
|
||||
(cond->> paragraphs
|
||||
(seq root-style) (into [root-style]))))
|
||||
|
||||
(defn- detach-typography
|
||||
"Drop the typography of runs when `k` is a style the typography sets, as
|
||||
the design panel does before changing it."
|
||||
[runs k]
|
||||
(if (contains? typography-styles k)
|
||||
(map (fn [[letters style]] [letters (dissoc style :typography)]) runs)
|
||||
runs))
|
||||
|
||||
(defn restyle
|
||||
"Set the run style `k` to `v` on every run of `text`."
|
||||
[text k v]
|
||||
(update-paragraphs text
|
||||
(fn [[style runs]]
|
||||
[style (map (fn [[letters run-style]] [letters (assoc run-style k v)])
|
||||
(detach-typography runs k))])))
|
||||
|
||||
(defn restyle-paragraphs
|
||||
"Set the paragraph style `k` to `v` on every paragraph of `text`."
|
||||
[text k v]
|
||||
(update-paragraphs text
|
||||
(fn [[style runs]]
|
||||
[(assoc style k v) (detach-typography runs k)])))
|
||||
|
||||
(defn set-valign
|
||||
[text v]
|
||||
(let [[root-style paragraphs] (split-styles text)]
|
||||
(into [(assoc root-style :valign v)] paragraphs)))
|
||||
|
||||
(defn set-letters
|
||||
"Replace the letters of `text` with `letters`, a line break per paragraph,
|
||||
all in the styles of its first paragraph and first run."
|
||||
[text letters]
|
||||
(let [[root-style paragraphs] (split-styles text)
|
||||
[p-style runs] (split-styles (first paragraphs))
|
||||
[_ run-style] (first runs)
|
||||
paragraphs (mapv (fn [line]
|
||||
(cond->> [[line run-style]]
|
||||
(seq p-style) (into [p-style])))
|
||||
(str/split letters #"\n" -1))]
|
||||
(cond->> paragraphs
|
||||
(seq root-style) (into [root-style]))))
|
||||
|
||||
(defn apply-typography
|
||||
"Apply the typography `label`, whose styles are `styles` (see
|
||||
`typography-styles`), to all of `text`."
|
||||
[text label styles]
|
||||
(update-paragraphs text
|
||||
(fn [[style runs]]
|
||||
[(cond-> style
|
||||
(contains? styles :line-height) (assoc :line-height (:line-height styles)))
|
||||
(map (fn [[letters run-style]]
|
||||
[letters (merge run-style
|
||||
(dissoc styles :line-height)
|
||||
{:typography label})])
|
||||
runs)])))
|
||||
|
||||
;; ---- The override rule for texts
|
||||
;;
|
||||
;; Which overrides a copy text must hold, given its content and its near
|
||||
;; main's: "different means touched" (`mem:common/component-sync-contract`)
|
||||
;; for the text model of decisions (k)-(o). The letters (with line breaks)
|
||||
;; are compared whole; each style unit is compared per letter, against the
|
||||
;; main's style placed onto the copy's letters as sync places it:
|
||||
;; 1. a unit with one value across the main applies to every letter;
|
||||
;; 2. a letter both texts share, in a stretch of at least
|
||||
;; `min-shared-stretch` equal letters found by a letter diff, takes the
|
||||
;; main's value on that letter;
|
||||
;; 3. any other letter takes the value of the letter before it, across line
|
||||
;; breaks; letters at the start take the first shared letter after them;
|
||||
;; with nothing shared, the main's first letter.
|
||||
;; Paragraph units go by paragraph: a paragraph takes the unit of the main's
|
||||
;; paragraph that holds its first shared letter, else follows the paragraph
|
||||
;; before it. Vertical alignment compares the roots.
|
||||
|
||||
(def min-shared-stretch 3)
|
||||
|
||||
(def run-units
|
||||
"Style unit -> the run attributes it covers."
|
||||
{:family [:font-family :font-id]
|
||||
:weight [:font-weight :font-style :font-variant-id]
|
||||
:size [:font-size]
|
||||
:spacing [:letter-spacing]
|
||||
:decoration [:text-decoration]
|
||||
:case [:text-transform]
|
||||
:fills [:fills]
|
||||
:typography [:typography-ref-id :typography-ref-file]})
|
||||
|
||||
(def paragraph-units
|
||||
{:align [:text-align]
|
||||
:direction [:text-direction]
|
||||
:line-height [:line-height]})
|
||||
|
||||
(defn- letters-of
|
||||
"The letters of `content`, one entry per letter with its paragraph index
|
||||
and run node; line breaks are `\\n` entries with no run."
|
||||
[content]
|
||||
(let [paragraphs (vec (txt/node-seq txt/is-paragraph-node? content))]
|
||||
(vec
|
||||
(mapcat (fn [i p]
|
||||
(concat (when (pos? i) [{:letter \newline :paragraph i}])
|
||||
(for [node (:children p)
|
||||
c (:text node)]
|
||||
{:letter c :paragraph i :node node})))
|
||||
(range)
|
||||
paragraphs))))
|
||||
|
||||
(defn- lcs-pairs
|
||||
"The pairs [i j] of equal letters of `a` and `b` along a longest common
|
||||
subsequence, in order."
|
||||
[a b]
|
||||
(let [n (count a) m (count b)
|
||||
table (reduce (fn [t [i j]]
|
||||
(assoc t [i j]
|
||||
(if (= (nth a i) (nth b j))
|
||||
(inc (get t [(inc i) (inc j)] 0))
|
||||
(max (get t [(inc i) j] 0) (get t [i (inc j)] 0)))))
|
||||
{}
|
||||
(for [i (range (dec n) -1 -1) j (range (dec m) -1 -1)] [i j]))]
|
||||
(loop [i 0 j 0 acc []]
|
||||
(cond
|
||||
(or (>= i n) (>= j m)) acc
|
||||
(= (nth a i) (nth b j)) (recur (inc i) (inc j) (conj acc [i j]))
|
||||
(>= (get table [(inc i) j] 0) (get table [i (inc j)] 0)) (recur (inc i) j acc)
|
||||
:else (recur i (inc j) acc)))))
|
||||
|
||||
(defn shared-letters
|
||||
"Index in `b` -> index in `a` of the letters the strings share, in
|
||||
stretches of at least `min-shared-stretch` consecutive letters."
|
||||
[a b]
|
||||
(let [stretches (->> (lcs-pairs (vec a) (vec b))
|
||||
(reduce (fn [acc [i j :as pair]]
|
||||
(let [[pi pj] (peek (peek acc))]
|
||||
(if (and pi (= i (inc pi)) (= j (inc pj)))
|
||||
(conj (pop acc) (conj (peek acc) pair))
|
||||
(conj acc [pair]))))
|
||||
[]))]
|
||||
(into {}
|
||||
(comp (filter #(>= (count %) min-shared-stretch))
|
||||
cat
|
||||
(map (fn [[i j]] [j i])))
|
||||
stretches)))
|
||||
|
||||
(defn- unit-value
|
||||
[node attrs]
|
||||
(let [v (select-keys node attrs)]
|
||||
;; a default the editors leave out is the same as the default written
|
||||
(cond-> v
|
||||
(and (contains? (set attrs) :fills) (nil? (:fills v))) (assoc :fills nil))))
|
||||
|
||||
(defn- place-values
|
||||
"The values of the target's letters, from `source-values` (one per source
|
||||
letter, nil for line breaks) by rules 1-3, given `shared` (target index ->
|
||||
source index) and the target's line breaks `breaks?` (per letter)."
|
||||
[source-values shared breaks?]
|
||||
(let [values (remove nil? source-values)
|
||||
n (count breaks?)]
|
||||
(if (<= (count (distinct values)) 1)
|
||||
(mapv (fn [b] (when-not b (first values))) breaks?)
|
||||
(let [direct (mapv (fn [i] (when-not (nth breaks? i)
|
||||
(some->> (get shared i) (nth source-values))))
|
||||
(range n))
|
||||
first-shared (some (fn [i] (when (some? (nth direct i)) (nth direct i))) (range n))
|
||||
fallback (or first-shared (first values))]
|
||||
(first
|
||||
(reduce (fn [[acc prev] i]
|
||||
(cond
|
||||
(nth breaks? i) [(conj acc nil) prev]
|
||||
(some? (nth direct i)) [(conj acc (nth direct i)) (nth direct i)]
|
||||
:else (let [v (or prev fallback)] [(conj acc v) v])))
|
||||
[[] nil]
|
||||
(range n)))))))
|
||||
|
||||
(defn override-units
|
||||
"The overrides a copy whose content is `copy` must hold against its near
|
||||
main's content `main`: `:letters`, `:valign`, and the keys of `run-units`
|
||||
and `paragraph-units` whose values differ from the main's placed onto the
|
||||
copy's letters."
|
||||
[main copy]
|
||||
(let [ml (letters-of main)
|
||||
cl (letters-of copy)
|
||||
shared (shared-letters (map :letter ml) (map :letter cl))
|
||||
breaks? (mapv #(nil? (:node %)) cl)
|
||||
main-ps (vec (txt/node-seq txt/is-paragraph-node? main))
|
||||
copy-ps (vec (txt/node-seq txt/is-paragraph-node? copy))
|
||||
run-diff (for [[unit attrs] run-units
|
||||
:let [source (mapv #(some-> (:node %) (unit-value attrs)) ml)
|
||||
placed (place-values source shared breaks?)
|
||||
actual (mapv #(some-> (:node %) (unit-value attrs)) cl)]
|
||||
:when (not= placed actual)]
|
||||
unit)
|
||||
;; a paragraph's first shared letter names the main paragraph it
|
||||
;; follows
|
||||
para-of (fn [i]
|
||||
(some (fn [[k {:keys [paragraph]}]]
|
||||
(when (= paragraph i)
|
||||
(some->> (get shared k) (nth ml) :paragraph)))
|
||||
(map-indexed vector cl)))
|
||||
para-diff (for [[unit attrs] paragraph-units
|
||||
:let [values (mapv #(unit-value % attrs) main-ps)
|
||||
placed (if (<= (count (distinct values)) 1)
|
||||
(vec (repeat (count copy-ps) (first values)))
|
||||
(first
|
||||
(reduce (fn [[acc prev] i]
|
||||
(let [v (or (some->> (para-of i) (nth values))
|
||||
prev
|
||||
(first values))]
|
||||
[(conj acc v) v]))
|
||||
[[] nil]
|
||||
(range (count copy-ps)))))
|
||||
actual (mapv #(unit-value % attrs) copy-ps)]
|
||||
:when (not= placed actual)]
|
||||
unit)]
|
||||
(cond-> (into #{} (concat run-diff para-diff))
|
||||
(not= (txt/content->text main) (txt/content->text copy))
|
||||
(conj :letters)
|
||||
|
||||
(not= (:vertical-align main "top") (:vertical-align copy "top"))
|
||||
(conj :valign))))
|
||||
@ -101,6 +101,9 @@
|
||||
(t/deftest case-y-text-override-merges-with-main-text-change
|
||||
(check (cases/text-override-merges-with-main-text-change)))
|
||||
|
||||
(t/deftest case-ba-text-style-override-against-main-style-change
|
||||
(check (cases/text-style-override-against-main-style-change)))
|
||||
|
||||
(t/deftest case-aa-copy-swap-survives-main-changes
|
||||
(check (cases/copy-swap-survives-main-changes)))
|
||||
|
||||
|
||||
@ -145,3 +145,10 @@
|
||||
(sync-file :components (thi/id :component1)))]
|
||||
;; the touched text attributes block it
|
||||
(tm/known-failure "F3" "40" "24" (font-size-of (ths/get-shape file' :copy-child)))))
|
||||
|
||||
(t/deftest library-typography-sync-writes-only-text-styles
|
||||
;; the sync merges the whole typography into each node, its own
|
||||
;; :modified-at included
|
||||
(let [file' (change-library-typography (component-using-typography) "24")
|
||||
node (get-in (ths/get-shape file' :main-child) [:content :children 0 :children 0 :children 0])]
|
||||
(tm/known-failure "F80" false true (contains? node :modified-at))))
|
||||
|
||||
175
common/test/common_tests/logic/text_editor_output_test.cljc
Normal file
175
common/test/common_tests/logic/text_editor_output_test.cljc
Normal file
@ -0,0 +1,175 @@
|
||||
;; 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.logic.text-editor-output-test
|
||||
"What the text editors commit when a copy text is edited, recorded in the
|
||||
workspace: the WASM editor (WebGL renderer) and the v1 editor (SVG
|
||||
renderer). The DOM editor is only on together with the WASM one, which
|
||||
the viewport prefers, so no session runs it. Each fixture is the
|
||||
`:content` of one commit, built key for key as the editor wrote it.
|
||||
Replaying the commits on a copy gives the touched groups the workspace
|
||||
showed, less `:name-group`, which the automatic rename of the frontend
|
||||
adds (F27)."
|
||||
(:require
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.test-helpers.texts :as thtx]
|
||||
[app.common.types.text :as txt]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
;; ---- Fixtures
|
||||
|
||||
(def ^:private run-style
|
||||
"The run styles both editors write for a text with no styles given."
|
||||
{:line-height "1.2" :font-style "normal" :typography-ref-id nil :text-transform "none"
|
||||
:font-id "sourcesanspro" :font-size "14" :font-weight "400" :typography-ref-file nil
|
||||
:font-variant-id "regular" :text-decoration "none" :letter-spacing "0"
|
||||
:fills [{:fill-color "#000000" :fill-opacity 1}] :font-family "sourcesanspro"})
|
||||
|
||||
(def ^:private bold {:font-weight "700" :font-variant-id "bold"})
|
||||
|
||||
(def ^:private direction {:text-align "left" :text-direction "ltr"})
|
||||
|
||||
(defn- run
|
||||
[text & [style]]
|
||||
(merge run-style style {:text text}))
|
||||
|
||||
(defn- paragraph
|
||||
"A paragraph as the WASM editor writes it: direction only, the line height
|
||||
stays on the runs."
|
||||
[& runs]
|
||||
(assoc direction :type "paragraph" :children (vec runs)))
|
||||
|
||||
(defn- keyed
|
||||
"`paragraph` once the v1 editor has opened it."
|
||||
[paragraph key]
|
||||
(assoc paragraph :key key))
|
||||
|
||||
(defn- styled-paragraph
|
||||
"A paragraph as the v1 editor writes it when it restyles or adds one: the
|
||||
styles of its first run on the paragraph too."
|
||||
[key & runs]
|
||||
(merge run-style direction {:key key :type "paragraph" :children (vec runs)}))
|
||||
|
||||
(defn- text
|
||||
[& paragraphs]
|
||||
{:type "root"
|
||||
:children [{:type "paragraph-set" :children (vec paragraphs)}]
|
||||
:vertical-align "top"})
|
||||
|
||||
(def ^:private main-text
|
||||
"\"Hello world\" as the WASM editor writes a new text: the main of every copy below."
|
||||
(text (paragraph (run "Hello world"))))
|
||||
|
||||
(def ^:private v1-new-text
|
||||
"\"Hello world\" as the v1 editor writes a new text."
|
||||
{:type "root"
|
||||
:children [{:type "paragraph-set"
|
||||
:children [(merge run-style direction
|
||||
{:key "7aab0" :type "paragraph"
|
||||
:children [(merge (dissoc run-style :typography-ref-id :typography-ref-file)
|
||||
direction
|
||||
{:text "Hello world"})]})]}]})
|
||||
|
||||
(defn- keystrokes
|
||||
"The text after each key typed of `typed`, `f` building it from the
|
||||
letters typed so far: the WASM editor commits on every key."
|
||||
[typed f]
|
||||
(map #(f (subs typed 0 %)) (range 1 (inc (count typed)))))
|
||||
|
||||
(def ^:private edits
|
||||
"Edit -> editor -> the contents it committed, in order."
|
||||
{:type-in-place
|
||||
{:wasm (repeat 2 (text (paragraph (run "Hello world!"))))
|
||||
:v1 [(text (keyed (paragraph (run "Hello world!")) "2dp2k"))]}
|
||||
|
||||
:select-all-and-type
|
||||
{:wasm (concat (keystrokes "Buy now" #(text (paragraph (run %))))
|
||||
[(text (paragraph (run "Buy now")))])
|
||||
:v1 [(text (keyed (paragraph (run "Buy now")) "eid7b"))]}
|
||||
|
||||
:enter-and-type
|
||||
{:wasm (concat [(text (paragraph (run "Hello world")) (paragraph (run "")))]
|
||||
(keystrokes "Ends Sunday" #(text (paragraph (run "Hello world")) (paragraph (run %))))
|
||||
[(text (paragraph (run "Hello world")) (paragraph (run "Ends Sunday")))])
|
||||
:v1 [(text (-> (paragraph (run "Hello world")) (keyed "53n8g") (assoc :fills []))
|
||||
(styled-paragraph "dtt74" (merge (run "Ends Sunday") direction)))]}
|
||||
|
||||
:bold-a-word
|
||||
{:wasm (repeat 2 (text (paragraph (run "Hello ") (run "world" bold))))
|
||||
:v1 [(text (keyed (paragraph (run "Hello world")) "3qtf9"))
|
||||
(text (styled-paragraph "3qtf9" (run "Hello ") (run "world" bold)))]}})
|
||||
|
||||
(def ^:private shown
|
||||
"Edit -> the text it leaves, in the notation."
|
||||
{:type-in-place [[["Hello world!"]]]
|
||||
:select-all-and-type [[["Buy now"]]]
|
||||
:enter-and-type [[["Hello world"]] [["Ends Sunday"]]]
|
||||
:bold-a-word [[["Hello "] ["world" {:weight 700}]]]})
|
||||
|
||||
(def ^:private touched
|
||||
"Edit -> editor -> the touched groups of the copy text after it."
|
||||
{:type-in-place {:wasm #{:content-group :text-content-text}
|
||||
:v1 #{:content-group :text-content-text}}
|
||||
:select-all-and-type {:wasm #{:content-group :text-content-text}
|
||||
:v1 #{:content-group :text-content-text}}
|
||||
;; the WASM editor commits the new paragraph and then each key in it
|
||||
:enter-and-type {:wasm #{:content-group :text-content-text :text-content-structure}
|
||||
:v1 #{:content-group :text-content-structure}}
|
||||
;; the v1 editor also writes the run styles on the paragraph
|
||||
:bold-a-word {:wasm #{:content-group :text-content-structure}
|
||||
:v1 #{:content-group :text-content-attribute :text-content-structure}}})
|
||||
|
||||
(defn- copy-text-after
|
||||
"The copy text of a main written as `main`, once the copy gets `commits`."
|
||||
[main commits]
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame-with-text :main-root :main-child ""
|
||||
:child-params {:content main})
|
||||
(thc/make-component :component1 :main-root)
|
||||
(thc/instantiate-component :component1 :copy-root
|
||||
{:children-labels [:copy-child]}))]
|
||||
(ths/get-shape (reduce #(thtx/set-content %1 :copy-child %2) file commits)
|
||||
:copy-child)))
|
||||
|
||||
;; ---- Tests
|
||||
|
||||
(t/deftest the-notation-reads-what-the-editors-write
|
||||
(t/is (= [[["Hello world"]]] (thtx/view main-text)))
|
||||
(t/is (= [[["Hello world"]]] (thtx/view v1-new-text)))
|
||||
(doseq [[edit by-editor] edits
|
||||
[editor commits] by-editor]
|
||||
(t/testing (str edit " in the " editor " editor")
|
||||
(t/is (= (shown edit) (thtx/view (last commits)))))))
|
||||
|
||||
(t/deftest each-edit-touches-these-groups-of-a-copy
|
||||
(doseq [[edit by-editor] edits
|
||||
[editor commits] by-editor]
|
||||
(t/testing (str edit " in the " editor " editor")
|
||||
(let [copy (copy-text-after main-text commits)]
|
||||
(t/is (= (shown edit) (thtx/view (:content copy))))
|
||||
(t/is (= (get-in touched [edit editor]) (:touched copy)))
|
||||
(case edit
|
||||
(:type-in-place :select-all-and-type)
|
||||
(t/is (not (contains? (:touched copy) :text-content-attribute))
|
||||
"typing sets no style override")
|
||||
:bold-a-word
|
||||
(t/is (not (contains? (:touched copy) :text-content-text))
|
||||
"a style change sets no text override")
|
||||
nil)))))
|
||||
|
||||
(t/deftest a-text-in-one-style-from-either-editor-is-uniform
|
||||
;; WASM: the paragraph has a direction its runs lack; v1: the paragraph has
|
||||
;; typography refs (nil) its runs lack
|
||||
(doseq [content [main-text v1-new-text]]
|
||||
(tm/known-failure "F79" true false
|
||||
(txt/equal-attrs? content (txt/get-first-paragraph-text-attrs content)))))
|
||||
84
common/test/common_tests/logic/text_notation_test.cljc
Normal file
84
common/test/common_tests/logic/text_notation_test.cljc
Normal file
@ -0,0 +1,84 @@
|
||||
;; 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.logic.text-notation-test
|
||||
"The text notation of `app.common.test-helpers.texts`."
|
||||
(:require
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.test-helpers.texts :as thtx]
|
||||
[app.common.types.text :as txt]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
(t/deftest content-is-written-as-the-editors-write-it
|
||||
(let [content (thtx/content [[["Price: "] ["$10" {:weight 700 :color :red}]]])
|
||||
paragraph (first (txt/node-seq txt/is-paragraph-node? content))
|
||||
[run1 run2] (txt/node-seq txt/is-text-node? content)]
|
||||
(t/testing "every run carries every run style, fills included"
|
||||
(t/is (= (assoc thtx/base-style :text "Price: ") run1))
|
||||
(t/is (= (assoc thtx/base-style
|
||||
:text "$10"
|
||||
:font-weight "700"
|
||||
:font-variant-id "bold"
|
||||
:fills [{:fill-color "#FF0000" :fill-opacity 1}])
|
||||
run2)))
|
||||
(t/testing "a paragraph carries its styles and those of its first run"
|
||||
(t/is (= (merge thtx/base-style thtx/base-paragraph-style)
|
||||
(dissoc paragraph :type :children))))
|
||||
(t/testing "the root carries the vertical alignment"
|
||||
(t/is (= "top" (:vertical-align content))))))
|
||||
|
||||
(t/deftest view-reads-content-back-into-the-notation
|
||||
(thi/new-id! :file1)
|
||||
(thi/new-id! :body)
|
||||
(doseq [text [[[["Hello world"]]]
|
||||
[[["Price: "] ["$10" {:weight 700}]]]
|
||||
[[["Add to cart" {:family "Roboto" :weight 600 :italic true :size 18 :spacing 2
|
||||
:case "uppercase" :decoration "underline" :color :blue}]]]
|
||||
[{:valign "bottom"}
|
||||
[{:align "center" :direction "rtl" :line-height 1.5} ["Summer " {:weight 700}] ["sale"]]
|
||||
[["Ends Sunday"]]]
|
||||
[[["Read the guide" {:typography :body}]]]
|
||||
[[["odd" {:font-variant-id "bold"}]]]]]
|
||||
(t/is (= text (thtx/normalize text)))))
|
||||
|
||||
(t/deftest view-joins-runs-with-equal-styles
|
||||
(t/is (= [[["Hello world"]]]
|
||||
(thtx/normalize [[["Hello"] [" "] ["world"]]])))
|
||||
(t/is (= [[["Hello"]] [[""]]]
|
||||
(thtx/normalize [[["Hello"] [""]] [[""]]]))))
|
||||
|
||||
(t/deftest edits-of-a-text-in-the-notation
|
||||
(thi/new-id! :file1)
|
||||
(thi/new-id! :body)
|
||||
(let [typed (thtx/apply-typography [[["Read"]]] :body {:family "Roboto" :size 16 :line-height 1.6})]
|
||||
(t/is (= [[{:line-height 1.6} ["Read" {:family "Roboto" :size 16 :typography :body}]]]
|
||||
typed))
|
||||
(t/testing "changing a style the typography sets drops the typography"
|
||||
(t/is (= [[{:line-height 1.6} ["Read" {:family "Roboto" :size 20}]]]
|
||||
(thtx/restyle typed :size 20)))
|
||||
(t/is (= [[{:line-height 2} ["Read" {:family "Roboto" :size 16}]]]
|
||||
(thtx/restyle-paragraphs typed :line-height 2))))
|
||||
(t/testing "changing another style keeps it"
|
||||
(t/is (= [[{:line-height 1.6} ["Read" {:family "Roboto" :size 16 :typography :body :color :red}]]]
|
||||
(thtx/restyle typed :color :red)))))
|
||||
(t/is (= [[{:align "center"} ["Buy now" {:weight 700}]]
|
||||
[{:align "center"} ["Ends Sunday" {:weight 700}]]]
|
||||
(thtx/set-letters [[{:align "center"} ["Add " {:weight 700}] ["to cart"]]]
|
||||
"Buy now\nEnds Sunday")))
|
||||
(t/is (= [{:valign "bottom"} [["Hello"]]]
|
||||
(thtx/set-valign [[["Hello"]]] "bottom"))))
|
||||
|
||||
(t/deftest set-text-commits-the-content-through-the-change-path
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(thtx/add-text-component-with-copy [[["Hello world"]]])
|
||||
(thtx/set-text :copy-child [[["Hello world" {:color :red}]]]))]
|
||||
(t/is (= [[["Hello world" {:color :red}]]] (thtx/text-of file :copy-child)))
|
||||
(t/is (= #{:content-group :text-content-attribute}
|
||||
(:touched (ths/get-shape file :copy-child))))))
|
||||
138
common/test/common_tests/logic/text_override_rule_test.cljc
Normal file
138
common/test/common_tests/logic/text_override_rule_test.cljc
Normal file
@ -0,0 +1,138 @@
|
||||
;; 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.logic.text-override-rule-test
|
||||
"The override rule for texts (`thtx/override-units`): which overrides a
|
||||
copy text must hold against its main."
|
||||
(:require
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.texts :as thtx]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
(defn- overrides
|
||||
[main copy]
|
||||
(thtx/override-units (thtx/content main) (thtx/content copy)))
|
||||
|
||||
(t/deftest shared-letters-come-in-stretches-of-three
|
||||
(t/is (= {0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10}
|
||||
(thtx/shared-letters "Hello world" "Hello world!!!")))
|
||||
;; "llo world" is shared; the lone "H" is not
|
||||
(t/is (= (zipmap (range 2 11) (range 2 11))
|
||||
(thtx/shared-letters "Hello world" "Hallo world")))
|
||||
(t/is (= {} (thtx/shared-letters "OK" "Ok")))
|
||||
(t/is (= {} (thtx/shared-letters "Price: $10" "Sale"))))
|
||||
|
||||
(def ^:private price [[["Price: "] ["$10" {:weight 700}]]])
|
||||
|
||||
(t/deftest an-edit-overrides-only-what-it-changes
|
||||
(t/testing "typing at the end changes the letters only"
|
||||
(t/is (= #{:letters} (overrides [[["Hello world"]]] [[["Hello world!!!"]]]))))
|
||||
(t/testing "retyping all of a two-style text changes the letters only"
|
||||
(t/is (= #{:letters} (overrides price [[["Sale"]]]))))
|
||||
(t/testing "a new line in the style of the letter before it changes the letters only"
|
||||
(t/is (= #{:letters}
|
||||
(overrides [[["Summer " {:weight 700}] ["sale"]]]
|
||||
[[["Summer " {:weight 700}] ["sale"]] [["Ends Sunday"]]]))))
|
||||
(t/testing "bolding a word changes the weight only"
|
||||
(t/is (= #{:weight} (overrides [[["Hello world"]]] [[["Hello" {:weight 700}] [" world"]]]))))
|
||||
(t/testing "each style is a unit of its own"
|
||||
(doseq [[unit copy] {:fills [[["Hello world" {:color :red}]]]
|
||||
:family [[["Hello world" {:family "Roboto"}]]]
|
||||
:size [[["Hello world" {:size 18}]]]
|
||||
:spacing [[["Hello world" {:spacing 2}]]]
|
||||
:decoration [[["Hello world" {:decoration "underline"}]]]
|
||||
:case [[["Hello world" {:case "uppercase"}]]]
|
||||
:align [[{:align "center"} ["Hello world"]]]
|
||||
:direction [[{:direction "rtl"} ["Hello world"]]]
|
||||
:line-height [[{:line-height 1.5} ["Hello world"]]]
|
||||
:valign [{:valign "bottom"} [["Hello world"]]]}]
|
||||
(t/is (= #{unit} (overrides [[["Hello world"]]] copy)) (str unit))))
|
||||
(t/testing "a typography sets each unit it changes, and its reference"
|
||||
(thi/new-id! :file1)
|
||||
(thi/new-id! :heading)
|
||||
(t/is (= #{:family :weight :size :spacing :case :line-height :typography}
|
||||
(overrides [[["Hello world"]]]
|
||||
(thtx/apply-typography [[["Hello world"]]] :heading
|
||||
{:family "Inter" :weight 700 :size 24 :spacing 1
|
||||
:case "uppercase" :line-height 1.4}))))))
|
||||
|
||||
(t/deftest the-expected-copies-hold-exactly-their-overrides
|
||||
;; The copies `text-override-sync-test` expects after a sync, against the
|
||||
;; main they synced from: each must hold the overrides of its own edits
|
||||
;; and nothing else.
|
||||
(doseq [[title main copy expected]
|
||||
[["bold word, main blue"
|
||||
[[["Hello world" {:color :blue}]]]
|
||||
[[["Hello" {:weight 700 :color :blue}] [" world" {:color :blue}]]]
|
||||
#{:weight}]
|
||||
["bold word, main letters"
|
||||
[[["Hello there"]]]
|
||||
[[["Hello" {:weight 700}] [" there"]]]
|
||||
#{:weight}]
|
||||
["bold word, main replaces it"
|
||||
[[["Goodbye world"]]]
|
||||
[[["Goodbye world"]]]
|
||||
#{}]
|
||||
["bold word, main changes one letter"
|
||||
[[["Hallo world"]]]
|
||||
[[["Hallo" {:weight 700}] [" world"]]]
|
||||
#{:weight}]
|
||||
["bold letter of a short label"
|
||||
[[["Ok"]]]
|
||||
[[["Ok"]]]
|
||||
#{}]
|
||||
["retyped price, main family"
|
||||
[[["Price: " {:family "Roboto"}] ["$10" {:family "Roboto" :weight 700}]]]
|
||||
[[["Sale" {:family "Roboto"}]]]
|
||||
#{:letters}]
|
||||
["new price, main bolds Price"
|
||||
[[["Price" {:weight 700}] [": "] ["$10" {:weight 700}]]]
|
||||
[[["Price" {:weight 700}] [": "] ["$20" {:weight 700}]]]
|
||||
#{:letters}]
|
||||
["added letters, main colors a word"
|
||||
[[["Hello "] ["world" {:color :red}]]]
|
||||
[[["Hello "] ["world!!!" {:color :red}]]]
|
||||
#{:letters}]
|
||||
["new line, main family"
|
||||
[[["Summer " {:family "Roboto" :weight 700}] ["sale" {:family "Roboto"}]]]
|
||||
[[["Summer " {:family "Roboto" :weight 700}] ["sale" {:family "Roboto"}]]
|
||||
[["Ends Sunday" {:family "Roboto"}]]]
|
||||
#{:letters}]
|
||||
["across runs, main colors the price"
|
||||
[[["Price: "] ["$10" {:weight 700 :color :red}]]]
|
||||
[[["Cost: "] ["$10" {:weight 700 :color :red}]]]
|
||||
#{:letters}]
|
||||
["worked example, main colors a word"
|
||||
[[["Hello "] ["world" {:color :red}]]]
|
||||
[[["Hello" {:weight 700}] [" "] ["world!!!" {:color :red}]]]
|
||||
#{:letters :weight}]
|
||||
["worked example, main centers"
|
||||
[[{:align "center"} ["Hello world"]]]
|
||||
[[{:align "center"} ["Hello" {:weight 700}] [" world!!!"]]]
|
||||
#{:letters :weight}]
|
||||
["worked example, main light"
|
||||
[[["Hello world" {:weight 300}]]]
|
||||
[[["Hello" {:weight 700}] [" world!!!"]]]
|
||||
#{:letters :weight}]]]
|
||||
(t/is (= expected (overrides main copy)) title)))
|
||||
|
||||
(t/deftest wrong-sync-results-show-as-overrides
|
||||
;; What today's sync leaves (pinned in `text-override-sync-test`) differs
|
||||
;; from the main where the copy has no override: a scanner finds it.
|
||||
(t/testing "F78: the bold lands on the wrong letters"
|
||||
(t/is (= #{:letters :weight}
|
||||
(overrides [[["Price" {:weight 700}] [": "] ["$10" {:weight 700}]]]
|
||||
[[["Price: " {:weight 700}] ["$20"]]]))))
|
||||
(t/testing "F78: the main's red never reaches the shared word"
|
||||
(t/is (= #{:letters :fills}
|
||||
(overrides [[["Hello "] ["world" {:color :red}]]]
|
||||
[[["Hello world!!!"]]]))))
|
||||
(t/testing "F72: a recolored copy keeps the old size"
|
||||
(t/is (= #{:fills :size}
|
||||
(overrides [[["Add to cart" {:size 18}]]]
|
||||
[[["Add to cart" {:color :red}]]])))))
|
||||
140
common/test/common_tests/logic/text_override_switch_test.cljc
Normal file
140
common/test/common_tests/logic/text_override_switch_test.cljc
Normal file
@ -0,0 +1,140 @@
|
||||
;; 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.logic.text-override-switch-test
|
||||
"What a copy text holds after a variant switch that keeps overrides
|
||||
(`mem:common/component-sync-contract`). Each override (the letters, and
|
||||
each style) is carried only when the copy has it and the old and new
|
||||
variants agree on it; everything else comes from the new variant. Texts
|
||||
are written in the notation of `app.common.test-helpers.texts`."
|
||||
(:require
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.test-helpers.texts :as thtx]
|
||||
[app.common.test-helpers.variants :as thv]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
(defn- switch-text
|
||||
"The copy's text when a variant set has the members `old` and `new`, a
|
||||
copy of `old` writes `copy-text` and switches to `new`, keeping its
|
||||
overrides."
|
||||
[old copy-text new]
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant-with-text :v01 :c01 :m01 :c02 :m02 :t01 :t02 "" ""
|
||||
:text1-params {:content (thtx/content old)}
|
||||
:text2-params {:content (thtx/content new)})
|
||||
(thc/instantiate-component :c01 :copy :children-labels [:copy-t])
|
||||
(thtx/set-text :copy-t copy-text)
|
||||
(tho/swap-component-in-shape :copy :c02 {:new-shape-label :copy-2
|
||||
:keep-touched? true}))
|
||||
copy-t (first (:shapes (ths/get-shape file :copy-2)))]
|
||||
(thtx/view (:content (ths/get-shape-by-id file copy-t)))))
|
||||
|
||||
;; ---- Whole-text copy edits against pairs of variants
|
||||
|
||||
(def ^:private small [[["Add to cart"]]])
|
||||
|
||||
(def ^:private copy-edits
|
||||
"Copy edit -> the overrides it makes, each as the edit that writes it."
|
||||
{:letters {:letters #(thtx/set-letters % "Buy now")}
|
||||
:new-paragraph {:letters #(thtx/set-letters % "Add to cart\nEnds Sunday")}
|
||||
:color {:color #(thtx/restyle % :color :red)}
|
||||
:family {:family #(thtx/restyle % :family "Roboto")}
|
||||
:size {:size #(thtx/restyle % :size 18)}
|
||||
:weight {:weight #(thtx/restyle % :weight 700)}
|
||||
:align {:align #(thtx/restyle-paragraphs % :align "center")}
|
||||
:valign {:valign #(thtx/set-valign % "bottom")}
|
||||
:letters-and-color {:letters #(thtx/set-letters % "Buy now")
|
||||
:color #(thtx/restyle % :color :red)}})
|
||||
|
||||
(def ^:private variants
|
||||
"Second variant -> [its text, the overrides the two variants disagree on]."
|
||||
{:same [small #{}]
|
||||
:letters [[[["Add to bag"]]] #{:letters}]
|
||||
:size [[[["Add to cart" {:size 24}]]] #{:size}]
|
||||
:color [[[["Add to cart" {:color :blue}]]] #{:color}]
|
||||
:new-line [[[["Add to cart"]] [["Free shipping"]]] #{:letters}]
|
||||
:bold-word [[[["Add to "] ["cart" {:weight 700}]]] #{:weight}]})
|
||||
|
||||
(def ^:private known-failures
|
||||
"Copy edit -> [bug, how the copy ends, the variants it fails against].
|
||||
`:variant`: the copy takes the new variant's text whole; a text in the
|
||||
notation: the copy ends with that text."
|
||||
{:letters [["F81" :variant #{:bold-word}]]
|
||||
:new-paragraph [["F79" :variant #{:size :color}]
|
||||
["F81" :variant #{:bold-word}]]
|
||||
:color [["F77" :variant #{:size}]
|
||||
["F79" :variant #{:new-line}]
|
||||
["F81" :variant #{:bold-word}]]
|
||||
:family [["F77" :variant #{:size :color}]
|
||||
["F79" :variant #{:new-line}]
|
||||
["F81" :variant #{:bold-word}]]
|
||||
:size [["F77" :variant #{:color}]
|
||||
["F79" :variant #{:new-line}]
|
||||
["F81" :variant #{:bold-word}]]
|
||||
:weight [["F77" :variant #{:size :color}]
|
||||
["F79" :variant #{:new-line}]]
|
||||
:letters-and-color [["F77" [[["Buy now" {:size 24}]]] #{:size}]
|
||||
["F79" :variant #{:new-line}]
|
||||
["F81" :variant #{:bold-word}]]
|
||||
:align [["F76" :variant (disj (set (keys variants)) :same)]]
|
||||
:valign [["F76" :variant (disj (set (keys variants)) :same)]]})
|
||||
|
||||
(defn- known-failure
|
||||
[copy-edit variant]
|
||||
(some (fn [[bug ends variants]]
|
||||
(when (contains? variants variant) [bug ends]))
|
||||
(get known-failures copy-edit)))
|
||||
|
||||
(t/deftest whole-text-copy-edits-across-a-switch
|
||||
(doseq [[copy-title overrides] copy-edits
|
||||
[variant-title [new disagree]] variants]
|
||||
(t/testing (str "copy " copy-title ", variant " variant-title)
|
||||
(let [copy-text (reduce (fn [text f] (f text)) small (vals overrides))
|
||||
carried (remove (comp disagree key) overrides)
|
||||
expected (thtx/normalize (reduce (fn [text [_ f]] (f text)) new carried))
|
||||
actual (switch-text small copy-text new)]
|
||||
(if-let [[bug ends] (known-failure copy-title variant-title)]
|
||||
(tm/known-failure bug expected
|
||||
(thtx/normalize (if (= ends :variant) new ends))
|
||||
actual)
|
||||
(t/is (= expected actual)))))))
|
||||
|
||||
|
||||
;; ---- Cards
|
||||
|
||||
(t/deftest relabeled-copy-keeps-its-label-in-a-variant-with-a-bolder-word
|
||||
;; both variants have the same letters, so the label stays; the weights
|
||||
;; come from the new variant, and "Buy now" shares no letters with it, so
|
||||
;; all of it takes the weight of its first letter
|
||||
(let [old [[["Add to cart" {:weight 600}]]]
|
||||
new [[["Add to " {:weight 600}] ["cart" {:weight 700}]]]]
|
||||
(tm/known-failure "F81"
|
||||
[[["Buy now" {:weight 600}]]]
|
||||
new
|
||||
(switch-text old [[["Buy now" {:weight 600}]]] new))))
|
||||
|
||||
(t/deftest recolored-copy-keeps-its-color-from-small-to-large
|
||||
(let [old [[["Add to cart" {:weight 600}]]]
|
||||
new [[["Add to cart" {:weight 600 :size 24}]]]]
|
||||
(tm/known-failure "F77"
|
||||
[[["Add to cart" {:weight 600 :size 24 :color :red}]]]
|
||||
new
|
||||
(switch-text old [[["Add to cart" {:weight 600 :color :red}]]] new))))
|
||||
|
||||
(t/deftest copy-that-bolds-a-word-keeps-it-from-small-to-large
|
||||
(let [old [[["Hello world"]]]
|
||||
new [[["Hello world" {:size 24}]]]]
|
||||
(tm/known-failure "F77"
|
||||
[[["Hello" {:weight 700 :size 24}] [" world" {:size 24}]]]
|
||||
new
|
||||
(switch-text old [[["Hello" {:weight 700}] [" world"]]] new))))
|
||||
481
common/test/common_tests/logic/text_override_sync_test.cljc
Normal file
481
common/test/common_tests/logic/text_override_sync_test.cljc
Normal file
@ -0,0 +1,481 @@
|
||||
;; 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.logic.text-override-sync-test
|
||||
"What a copy text holds after its main changes and syncs, when the copy
|
||||
has its own letters or styles (`mem:common/component-sync-contract`):
|
||||
- letters and line breaks are one override, and each style another;
|
||||
- a copy's style override covers the whole text;
|
||||
- the main's styles go onto letters that differ from its own by three
|
||||
rules: a style with one value across the main's text applies to every
|
||||
letter; otherwise letters both texts share (a stretch of at least 3)
|
||||
take the main's style; any other letter takes the style of the letter
|
||||
before it, or at the start of the text, of the first shared letter
|
||||
after it, or with nothing shared, of the main's first letter.
|
||||
Texts are written in the notation of `app.common.test-helpers.texts`."
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.logic.libraries :as cll]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.test-helpers.texts :as thtx]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.types.typographies-list :as cttl]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
(def ^:private heading
|
||||
{:family "Inter" :weight 700 :size 24 :spacing 1 :case "uppercase" :line-height 1.4})
|
||||
|
||||
(def ^:private body
|
||||
{:family "Roboto" :weight 300 :size 16 :spacing 0.5 :case "capitalize" :line-height 1.6})
|
||||
|
||||
(defn- sample-file
|
||||
"A file with the typographies :heading and :body."
|
||||
[]
|
||||
(-> (thf/sample-file :file1)
|
||||
(ths/add-sample-typography :heading)
|
||||
(ths/add-sample-typography :body)))
|
||||
|
||||
(defn- sync-text
|
||||
"The copy's text when the main starts as `main`, the copy writes
|
||||
`copy-text` (nil for no edit), then the main writes `main-text` and syncs."
|
||||
[main copy-text main-text]
|
||||
(-> (sample-file)
|
||||
(thtx/add-text-component-with-copy main)
|
||||
(cond-> (some? copy-text) (thtx/set-text :copy-child copy-text))
|
||||
(thtx/set-text :main-child main-text)
|
||||
(tho/propagate-component-changes :component1)
|
||||
(thtx/text-of :copy-child)))
|
||||
|
||||
;; ---- Whole-text edits: every copy edit against every main edit
|
||||
|
||||
(def ^:private base [[["Hello world"]]])
|
||||
|
||||
(def ^:private copy-edits
|
||||
{:letters #(thtx/set-letters % "Buy now")
|
||||
:new-paragraph #(thtx/set-letters % "Hello world\nEnds Sunday")
|
||||
:color #(thtx/restyle % :color :red)
|
||||
:family #(thtx/restyle % :family "Roboto")
|
||||
:size #(thtx/restyle % :size 18)
|
||||
:weight #(thtx/restyle % :weight 700)
|
||||
:spacing #(thtx/restyle % :spacing 2)
|
||||
:decoration #(thtx/restyle % :decoration "underline")
|
||||
:case #(thtx/restyle % :case "uppercase")
|
||||
:line-height #(thtx/restyle-paragraphs % :line-height 1.5)
|
||||
:align #(thtx/restyle-paragraphs % :align "center")
|
||||
:direction #(thtx/restyle-paragraphs % :direction "rtl")
|
||||
:valign #(thtx/set-valign % "bottom")
|
||||
:letters-and-color #(-> % (thtx/set-letters "Buy now") (thtx/restyle :color :red))
|
||||
:typography #(thtx/apply-typography % :heading heading)})
|
||||
|
||||
(def ^:private main-edits
|
||||
{:letters #(thtx/set-letters % "Add to bag")
|
||||
:new-paragraph #(thtx/set-letters % "Hello world\nSecond line")
|
||||
:color #(thtx/restyle % :color :blue)
|
||||
:family #(thtx/restyle % :family "Inter")
|
||||
:size #(thtx/restyle % :size 24)
|
||||
:weight #(thtx/restyle % :weight 300)
|
||||
:spacing #(thtx/restyle % :spacing 1)
|
||||
:decoration #(thtx/restyle % :decoration "line-through")
|
||||
:case #(thtx/restyle % :case "lowercase")
|
||||
:line-height #(thtx/restyle-paragraphs % :line-height 2)
|
||||
:align #(thtx/restyle-paragraphs % :align "right")
|
||||
:direction #(thtx/restyle-paragraphs % :direction "rtl")
|
||||
:valign #(thtx/set-valign % "center")
|
||||
:typography #(thtx/apply-typography % :body body)})
|
||||
|
||||
(def ^:private style-edits
|
||||
#{:color :family :size :weight :spacing :decoration :case :line-height
|
||||
:align :direction :valign :typography})
|
||||
|
||||
(def ^:private known-failures
|
||||
"Copy edit -> [bug, how the copy ends, the main edits it fails against].
|
||||
`:as-it-was`: the sync leaves the copy as the copy edit left it.
|
||||
`:main`: the copy takes the main's text whole."
|
||||
(let [one-style (fn [own]
|
||||
[["F72" :as-it-was (disj style-edits own)]
|
||||
["F79" :as-it-was #{:new-paragraph}]])]
|
||||
{:color (one-style :color)
|
||||
:family (one-style :family)
|
||||
:size (one-style :size)
|
||||
:weight (one-style :weight)
|
||||
:spacing (one-style :spacing)
|
||||
:decoration (one-style :decoration)
|
||||
:case (one-style :case)
|
||||
:line-height (one-style :line-height)
|
||||
:typography [["F72" :as-it-was #{:color :decoration :align :direction :valign}]
|
||||
["F79" :as-it-was #{:new-paragraph}]]
|
||||
:new-paragraph [["F79" :as-it-was style-edits]]
|
||||
:letters-and-color [["F73" :as-it-was (disj style-edits :color)]]
|
||||
:align [["F76" :main (set (keys main-edits))]]
|
||||
:direction [["F76" :main (disj (set (keys main-edits)) :direction)]]
|
||||
:valign [["F76" :main (set (keys main-edits))]]}))
|
||||
|
||||
(defn- known-failure
|
||||
[copy-edit main-edit]
|
||||
(some (fn [[bug ends main-edits]]
|
||||
(when (contains? main-edits main-edit) [bug ends]))
|
||||
(get known-failures copy-edit)))
|
||||
|
||||
(t/deftest whole-text-copy-edits-against-main-edits
|
||||
;; Whole-text edits commute with the rules: the copy keeps what it
|
||||
;; edited and takes the rest from the main, which is its edit applied
|
||||
;; over the main's new text.
|
||||
(doseq [[copy-title copy-edit] copy-edits
|
||||
[main-title main-edit] main-edits]
|
||||
(t/testing (str "copy " copy-title ", main " main-title)
|
||||
(let [actual (sync-text base (copy-edit base) (main-edit base))
|
||||
expected (thtx/normalize (copy-edit (main-edit base)))]
|
||||
(if-let [[bug ends] (known-failure copy-title main-title)]
|
||||
(tm/known-failure bug expected
|
||||
(thtx/normalize (case ends
|
||||
:as-it-was (copy-edit base)
|
||||
:main (main-edit base)))
|
||||
actual)
|
||||
(t/is (= expected actual)))))))
|
||||
|
||||
;; ---- One style no longer blocks the others
|
||||
|
||||
(t/deftest recolored-copy-takes-the-main-font-and-size
|
||||
(let [main [[["Add to cart" {:weight 600}]]]
|
||||
copy [[["Add to cart" {:weight 600 :color :red}]]]
|
||||
main' [[["Add to cart" {:weight 600 :family "Roboto" :size 18}]]]
|
||||
actual (sync-text main copy main')]
|
||||
(tm/known-failure "F72"
|
||||
[[["Add to cart" {:family "Roboto" :weight 600 :size 18 :color :red}]]]
|
||||
copy
|
||||
actual)))
|
||||
|
||||
(t/deftest relabeled-and-recolored-copy-takes-the-main-size
|
||||
(let [main [[["Add to cart" {:weight 600}]]]
|
||||
copy [[["Buy now" {:weight 600 :color :red}]]]
|
||||
main' [[["Add to cart" {:weight 600 :size 18}]]]
|
||||
actual (sync-text main copy main')]
|
||||
(tm/known-failure "F73"
|
||||
[[["Buy now" {:weight 600 :size 18 :color :red}]]]
|
||||
copy
|
||||
actual)))
|
||||
|
||||
(t/deftest centered-copy-stays-centered
|
||||
(let [main [[["Free shipping"]]]
|
||||
copy [[{:align "center"} ["Free shipping"]]]
|
||||
main' [[["Free shipping" {:size 18}]]]
|
||||
actual (sync-text main copy main')]
|
||||
(tm/known-failure "F76"
|
||||
[[{:align "center"} ["Free shipping" {:size 18}]]]
|
||||
main'
|
||||
actual)))
|
||||
|
||||
(t/deftest copy-with-a-font-size-token-takes-the-main-color
|
||||
(let [main [[["Add to cart" {:weight 600}]]]
|
||||
file (-> (sample-file)
|
||||
(thtx/add-text-component-with-copy main))
|
||||
page (thf/current-page file)
|
||||
;; applying a token writes its value into the content and records it
|
||||
file (thf/apply-changes
|
||||
file
|
||||
(cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(thi/id :copy-child)}
|
||||
#(assoc %
|
||||
:content (thtx/content [[["Add to cart" {:weight 600 :size 16}]]])
|
||||
:applied-tokens {:font-size "size.md"})
|
||||
(:objects page)
|
||||
{}))
|
||||
file' (-> file
|
||||
(thtx/set-text :main-child [[["Add to cart" {:weight 600 :color :blue}]]])
|
||||
(tho/propagate-component-changes :component1))]
|
||||
(tm/known-failure "F72"
|
||||
[[["Add to cart" {:weight 600 :size 16 :color :blue}]]]
|
||||
[[["Add to cart" {:weight 600 :size 16}]]]
|
||||
(thtx/text-of file' :copy-child))))
|
||||
|
||||
;; ---- Bolding a word
|
||||
|
||||
(def ^:private hello-bold [[["Hello" {:weight 700}] [" world"]]])
|
||||
|
||||
(t/deftest copy-that-bolds-a-word-takes-the-main-color
|
||||
(tm/known-failure "F74"
|
||||
[[["Hello" {:weight 700 :color :blue}] [" world" {:color :blue}]]]
|
||||
hello-bold
|
||||
(sync-text [[["Hello world"]]] hello-bold [[["Hello world" {:color :blue}]]])))
|
||||
|
||||
(t/deftest copy-that-bolds-a-word-takes-the-main-letters
|
||||
;; "Hello" is shared, so it stays bold; " there" follows the space
|
||||
(tm/known-failure "F74"
|
||||
[[["Hello" {:weight 700}] [" there"]]]
|
||||
hello-bold
|
||||
(sync-text [[["Hello world"]]] hello-bold [[["Hello there"]]])))
|
||||
|
||||
(t/deftest copy-that-bolds-a-word-takes-the-main-family
|
||||
(tm/known-failure "F74"
|
||||
[[["Hello" {:family "Roboto" :weight 700}] [" world" {:family "Roboto"}]]]
|
||||
hello-bold
|
||||
(sync-text [[["Hello world"]]] hello-bold [[["Hello world" {:family "Roboto"}]]])))
|
||||
|
||||
(t/deftest copy-that-bolds-a-word-the-main-replaces-loses-the-bold
|
||||
;; "Goodbye" shares no stretch of 3 letters with "Hello", so it takes the
|
||||
;; style of the first shared letter after it, the regular space
|
||||
(tm/known-failure "F74"
|
||||
[[["Goodbye world"]]]
|
||||
hello-bold
|
||||
(sync-text [[["Hello world"]]] hello-bold [[["Goodbye world"]]])))
|
||||
|
||||
(t/deftest copy-that-bolds-a-word-the-main-changes-by-one-letter
|
||||
;; "llo world" is shared; "Ha" at the start takes the bold "l" after it
|
||||
(tm/known-failure "F74"
|
||||
[[["Hallo" {:weight 700}] [" world"]]]
|
||||
hello-bold
|
||||
(sync-text [[["Hello world"]]] hello-bold [[["Hallo world"]]])))
|
||||
|
||||
(t/deftest copy-that-bolds-a-letter-of-a-short-label
|
||||
;; a lone shared letter carries nothing: with no stretch of 3 shared, all
|
||||
;; letters take the style of the copy's first letter
|
||||
(let [copy [[["O"] ["K" {:weight 700}]]]]
|
||||
(tm/known-failure "F74"
|
||||
[[["Ok"]]]
|
||||
copy
|
||||
(sync-text [[["OK"]]] copy [[["Ok"]]]))))
|
||||
|
||||
(t/deftest copy-that-bolds-a-word-keeps-its-weights-when-the-main-is-light
|
||||
;; an override covers the whole text, so "world" stays regular
|
||||
(t/is (= hello-bold
|
||||
(sync-text [[["Hello world"]]] hello-bold [[["Hello world" {:weight 300}]]]))))
|
||||
|
||||
;; ---- Copies with their own letters
|
||||
|
||||
(def ^:private price [[["Price: "] ["$10" {:weight 700}]]])
|
||||
|
||||
(t/deftest retyped-two-style-copy-takes-the-main-family
|
||||
;; the family has one value across the main, so it applies everywhere;
|
||||
;; "Sale" shares nothing with "Price: $10" and takes the main's first
|
||||
;; letter's weight
|
||||
(tm/known-failure "F74"
|
||||
[[["Sale" {:family "Roboto"}]]]
|
||||
[[["Sale"]]]
|
||||
(sync-text price [[["Sale"]]]
|
||||
[[["Price: " {:family "Roboto"}] ["$10" {:family "Roboto" :weight 700}]]])))
|
||||
|
||||
(t/deftest copy-that-removes-a-run-takes-the-main-color
|
||||
(let [copy [[["$10" {:weight 700}]]]]
|
||||
(tm/known-failure "F74"
|
||||
[[["$10" {:weight 700 :color :blue}]]]
|
||||
copy
|
||||
(sync-text price copy
|
||||
[[["Price: " {:color :blue}] ["$10" {:weight 700 :color :blue}]]]))))
|
||||
|
||||
(t/deftest copy-with-new-letters-in-a-run-takes-the-main-family
|
||||
(t/is (= [[["Price: " {:family "Roboto"}] ["$20" {:family "Roboto" :weight 700}]]]
|
||||
(sync-text price [[["Price: "] ["$20" {:weight 700}]]]
|
||||
[[["Price: " {:family "Roboto"}] ["$10" {:family "Roboto" :weight 700}]]]))))
|
||||
|
||||
(t/deftest copy-with-new-letters-across-runs-takes-a-partial-color
|
||||
;; ": $10" is shared, so "$10" takes the red; "Cost" at the start takes
|
||||
;; the black of ":"
|
||||
(t/is (= [[["Cost: "] ["$10" {:weight 700 :color :red}]]]
|
||||
(sync-text price [[["Cost: "] ["$10" {:weight 700}]]]
|
||||
[[["Price: "] ["$10" {:weight 700 :color :red}]]]))))
|
||||
|
||||
(t/deftest copy-with-added-letters-takes-a-partial-color
|
||||
;; "world" is shared, so it turns red; "!!!" follows the "d" before it
|
||||
(tm/known-failure "F78"
|
||||
[[["Hello "] ["world!!!" {:color :red}]]]
|
||||
[[["Hello world!!!"]]]
|
||||
(sync-text [[["Hello world"]]] [[["Hello world!!!"]]]
|
||||
[[["Hello "] ["world" {:color :red}]]])))
|
||||
|
||||
(t/deftest copy-with-a-new-price-takes-the-main-bold-on-the-right-letters
|
||||
;; "Price: $" is shared; "20" follows the bold "$"
|
||||
(tm/known-failure "F78"
|
||||
[[["Price" {:weight 700}] [": "] ["$20" {:weight 700}]]]
|
||||
[[["Price: " {:weight 700}] ["$20"]]]
|
||||
(sync-text price [[["Price: "] ["$20" {:weight 700}]]]
|
||||
[[["Price" {:weight 700}] [": "] ["$10" {:weight 700}]]])))
|
||||
|
||||
(t/deftest copy-that-adds-a-line-to-a-two-style-title-takes-the-main-family
|
||||
;; the new line follows the regular "sale" before it, across the break
|
||||
(let [main [[["Summer " {:weight 700}] ["sale"]]]
|
||||
copy [[["Summer " {:weight 700}] ["sale"]]
|
||||
[["Ends Sunday"]]]]
|
||||
(tm/known-failure "F74"
|
||||
[[["Summer " {:family "Roboto" :weight 700}] ["sale" {:family "Roboto"}]]
|
||||
[["Ends Sunday" {:family "Roboto"}]]]
|
||||
copy
|
||||
(sync-text main copy
|
||||
[[["Summer " {:family "Roboto" :weight 700}] ["sale" {:family "Roboto"}]]]))))
|
||||
|
||||
(t/deftest copy-with-a-new-line-in-content-without-fills-takes-the-main-color
|
||||
;; `txt/change-text` writes nodes with no :fills, and the merge only
|
||||
;; writes the keys the copy's nodes have
|
||||
(let [file (-> (sample-file)
|
||||
(tho/add-frame-with-text :main-root :main-child "hello world")
|
||||
(thc/make-component :component1 :main-root)
|
||||
(thc/instantiate-component :component1 :copy-root {:children-labels [:copy-child]}))
|
||||
page (thf/current-page file)
|
||||
edit (fn [file label f]
|
||||
(thf/apply-changes
|
||||
file
|
||||
(cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(thi/id label)} f (:objects page) {})))
|
||||
blue [{:fill-color "#0000FF" :fill-opacity 1}]
|
||||
file' (-> file
|
||||
(edit :copy-child (fn [shape]
|
||||
(update shape :content txt/change-text "hello world\nsecond line")))
|
||||
(edit :main-child (fn [shape]
|
||||
(update shape :content
|
||||
#(txt/transform-nodes txt/is-content-node?
|
||||
(fn [node] (assoc node :fills blue))
|
||||
%))))
|
||||
(tho/propagate-component-changes :component1))]
|
||||
(tm/known-failure "F75"
|
||||
[[["hello world" {:color :blue}]] [["second line" {:color :blue}]]]
|
||||
[[["hello world" {:fills nil}]] [["second line" {:fills nil}]]]
|
||||
(thtx/text-of file' :copy-child))))
|
||||
|
||||
;; ---- Typographies and library colors
|
||||
|
||||
(t/deftest copy-that-resizes-a-typography-text-takes-the-main-color
|
||||
;; the size comes from the copy, so the typography no longer describes
|
||||
;; the run and goes, also when the copy's content kept it
|
||||
(let [main (thtx/apply-typography [[["Read the guide"]]] :body body)
|
||||
copy (thtx/restyle main :size 20)
|
||||
kept (thtx/apply-typography [[["Read the guide"]]] :body (assoc body :size 20))
|
||||
main' (thtx/restyle main :color :blue)]
|
||||
(t/testing "detached in the panel before resizing"
|
||||
(tm/known-failure "F72"
|
||||
(thtx/normalize (thtx/restyle copy :color :blue))
|
||||
(thtx/normalize copy)
|
||||
(sync-text main copy main')))
|
||||
(t/testing "resized keeping the typography (plugin API)"
|
||||
(tm/known-failure "F72"
|
||||
(thtx/normalize (thtx/restyle copy :color :blue))
|
||||
(thtx/normalize kept)
|
||||
(sync-text main kept main')))))
|
||||
|
||||
(t/deftest copy-that-detached-a-typography-takes-a-library-change-through-its-main
|
||||
(let [body-attrs {:font-id "sourcesanspro" :font-family "sourcesanspro" :font-variant-id "regular"
|
||||
:font-size "14" :font-weight "400" :font-style "normal"
|
||||
:line-height "1.2" :letter-spacing "0" :text-transform "none"}
|
||||
file (-> (thf/sample-file :file1)
|
||||
(ths/add-sample-typography :body body-attrs)
|
||||
(thtx/add-text-component-with-copy [[["Read the guide" {:typography :body}]]])
|
||||
(thtx/set-text :copy-child [[["Read the guide" {:size 16}]]])
|
||||
(update :data cttl/update-typography (thi/id :body)
|
||||
assoc :font-family "Roboto" :font-id "gfont-roboto"))
|
||||
sync (fn [file asset-type asset-id]
|
||||
(thf/apply-changes
|
||||
file
|
||||
(cll/generate-sync-file-changes (pcb/empty-changes) nil asset-type (:id file)
|
||||
asset-id (:id file) {(:id file) file} (:id file))))
|
||||
file' (-> file
|
||||
(sync :typographies (thi/id :body))
|
||||
(sync :components (thi/id :component1)))]
|
||||
(tm/known-failure "F72"
|
||||
[[["Read the guide" {:family "Roboto" :size 16}]]]
|
||||
[[["Read the guide" {:size 16}]]]
|
||||
(thtx/text-of file' :copy-child))))
|
||||
|
||||
(t/deftest untouched-copy-with-a-library-color-takes-the-main-size
|
||||
;; the library color sync writes the copy's runs; it is derived, so the
|
||||
;; main's later size change must still arrive
|
||||
(let [brand (fn [color] [{:fill-color color :fill-opacity 1
|
||||
:fill-color-ref-id (thi/id :brand)
|
||||
:fill-color-ref-file (thi/id :file1)}])
|
||||
sync (fn [file asset-type asset-id]
|
||||
(thf/apply-changes
|
||||
file
|
||||
(cll/generate-sync-file-changes (pcb/empty-changes) nil asset-type (:id file)
|
||||
asset-id (:id file) {(:id file) file} (:id file))))
|
||||
file (-> (thf/sample-file :file1)
|
||||
(ths/add-sample-library-color :brand :color "#0000FF"))
|
||||
file' (-> file
|
||||
(thtx/add-text-component-with-copy [[["New arrivals" {:weight 600 :fills (brand "#0000FF")}]]])
|
||||
(assoc-in [:data :colors (thi/id :brand) :color] "#7B3FD1")
|
||||
(sync :colors (thi/id :brand))
|
||||
(thtx/set-text :main-child [[["New arrivals" {:weight 600 :size 18 :fills (brand "#7B3FD1")}]]])
|
||||
(sync :components (thi/id :component1)))]
|
||||
(tm/known-failure "F3"
|
||||
[[["New arrivals" {:weight 600 :size 18 :fills (brand "#7B3FD1")}]]]
|
||||
[[["New arrivals" {:weight 600 :fills (brand "#7B3FD1")}]]]
|
||||
(thtx/text-of file' :copy-child))))
|
||||
|
||||
;; ---- The worked example: a copy that bolds "Hello" and adds "!!!"
|
||||
|
||||
(def ^:private worked-copy [[["Hello" {:weight 700}] [" world!!!"]]])
|
||||
|
||||
(def ^:private worked-example
|
||||
"Main edit of \"Hello world\" -> what the copy should hold."
|
||||
[["colors all of it blue"
|
||||
[[["Hello world" {:color :blue}]]]
|
||||
[[["Hello" {:weight 700 :color :blue}] [" world!!!" {:color :blue}]]]]
|
||||
|
||||
["colors \"world\" red"
|
||||
[[["Hello "] ["world" {:color :red}]]]
|
||||
[[["Hello" {:weight 700}] [" "] ["world!!!" {:color :red}]]]]
|
||||
|
||||
["changes the size"
|
||||
[[["Hello world" {:size 18}]]]
|
||||
[[["Hello" {:weight 700 :size 18}] [" world!!!" {:size 18}]]]]
|
||||
|
||||
["centers it"
|
||||
[[{:align "center"} ["Hello world"]]]
|
||||
[[{:align "center"} ["Hello" {:weight 700}] [" world!!!"]]]]
|
||||
|
||||
["changes the letters to \"Hello there\""
|
||||
[[["Hello there"]]]
|
||||
worked-copy]
|
||||
|
||||
["changes the letters to \"Goodbye world\""
|
||||
[[["Goodbye world"]]]
|
||||
worked-copy]
|
||||
|
||||
["makes \"world\" light"
|
||||
[[["Hello "] ["world" {:weight 300}]]]
|
||||
worked-copy]
|
||||
|
||||
["makes all of it light"
|
||||
[[["Hello world" {:weight 300}]]]
|
||||
worked-copy]
|
||||
|
||||
["changes the family"
|
||||
[[["Hello world" {:family "Roboto"}]]]
|
||||
[[["Hello" {:family "Roboto" :weight 700}] [" world!!!" {:family "Roboto"}]]]]
|
||||
|
||||
["applies a typography"
|
||||
(thtx/apply-typography [[["Hello world"]]] :heading heading)
|
||||
;; the weight is the copy's, so the typography goes
|
||||
[[{:line-height 1.4}
|
||||
["Hello" {:family "Inter" :weight 700 :size 24 :spacing 1 :case "uppercase"}]
|
||||
[" world!!!" {:family "Inter" :size 24 :spacing 1 :case "uppercase"}]]]]])
|
||||
|
||||
(t/deftest copy-that-bolds-a-word-and-adds-letters-against-main-edits
|
||||
(doseq [[title main' expected] worked-example]
|
||||
(t/testing (str "the main " title)
|
||||
(let [actual (sync-text [[["Hello world"]]] worked-copy main')
|
||||
expected (thtx/normalize expected)]
|
||||
(if (= expected (thtx/normalize worked-copy))
|
||||
(t/is (= expected actual))
|
||||
(tm/known-failure "F73" expected (thtx/normalize worked-copy) actual))))))
|
||||
|
||||
;; ---- The same as today
|
||||
|
||||
(t/deftest relabeled-copy-takes-the-main-size
|
||||
(t/is (= [[["Buy now" {:weight 600 :size 18}]]]
|
||||
(sync-text [[["Add to cart" {:weight 600}]]]
|
||||
[[["Buy now" {:weight 600}]]]
|
||||
[[["Add to cart" {:weight 600 :size 18}]]]))))
|
||||
|
||||
(t/deftest relabeled-copy-keeps-its-letters-when-the-main-relabels
|
||||
(t/is (= [[["Buy now" {:weight 600}]]]
|
||||
(sync-text [[["Add to cart" {:weight 600}]]]
|
||||
[[["Buy now" {:weight 600}]]]
|
||||
[[["Add to bag" {:weight 600}]]]))))
|
||||
@ -77,6 +77,11 @@
|
||||
[common-tests.logic.swap-as-override-test]
|
||||
[common-tests.logic.swap-keeps-id-test]
|
||||
[common-tests.logic.swap-missing-library-test]
|
||||
[common-tests.logic.text-editor-output-test]
|
||||
[common-tests.logic.text-notation-test]
|
||||
[common-tests.logic.text-override-rule-test]
|
||||
[common-tests.logic.text-override-switch-test]
|
||||
[common-tests.logic.text-override-sync-test]
|
||||
[common-tests.logic.text-sync-test]
|
||||
[common-tests.logic.text-touched-test]
|
||||
[common-tests.logic.token-apply-test]
|
||||
@ -190,6 +195,11 @@
|
||||
'common-tests.logic.swap-as-override-test
|
||||
'common-tests.logic.swap-keeps-id-test
|
||||
'common-tests.logic.swap-missing-library-test
|
||||
'common-tests.logic.text-editor-output-test
|
||||
'common-tests.logic.text-notation-test
|
||||
'common-tests.logic.text-override-rule-test
|
||||
'common-tests.logic.text-override-switch-test
|
||||
'common-tests.logic.text-override-sync-test
|
||||
'common-tests.logic.text-sync-test
|
||||
'common-tests.logic.text-touched-test
|
||||
'common-tests.logic.token-apply-test
|
||||
|
||||
@ -81,6 +81,9 @@
|
||||
(t/deftest case-y-text-override-merges-with-main-text-change
|
||||
(t/async done (ftm/check done (cases/text-override-merges-with-main-text-change))))
|
||||
|
||||
(t/deftest case-ba-text-style-override-against-main-style-change
|
||||
(t/async done (ftm/check done (cases/text-style-override-against-main-style-change))))
|
||||
|
||||
(t/deftest case-n-geometry-sync-with-rotated-instances
|
||||
(t/async done (ftm/check done (cases/geometry-sync-with-rotated-instances))))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user