penpot/common/test/common_tests/logic/text_sync_test.cljc
alonso.torres ac7287d9cd ✨ Give each style of a copy text its own override
A copy text had three override subgroups: its letters, any style, and
its structure (the number of paragraphs or runs). One style override
kept every other style of the main out (F72), letters and a style
together kept everything out (F73), a structure change kept the main's
styles out unless both texts had one style (F74), which the editors'
content never had (F79), and alignment, direction and vertical
alignment overrides were lost at the next sync (F76).

Now a copy text has :text-content-text for its letters and line breaks
and one group per style unit: family, weight (with style and variant),
size, line height, spacing, decoration, case, fills, typography,
alignment, direction and vertical alignment (cttx/style-units). An edit
sets the groups of what it changes: a letter both texts share that
changes its style, or a style that was not in the text before, so typed
letters touch no style. A token in the content sets the groups of the
styles it sets, and token sync of a text follows them.

Sync and the keep-touched variant switch share one merge
(cttx/merge-overrides): the letters of one side, and each unit from its
side placed on them. A unit with one value applies everywhere; else
letters both texts share (stretches of three or more) take the source's
style, and the rest follow the letter before them. The switch carries
an override only when the old and new mains agree on it (F77, F81), and
the copy then holds the groups its result differs in. A typography
reference stays only when every unit it sets comes from its side. The
variant id goes with the weight; where the family comes from the other
side, the frontend has to pick the family's closest variant.

app.common.types.text reads a content as letters, each with its run and
paragraph, and builds the content back from them; it gives the style of
one unit for a letter, compares styles by what they show, and finds the
letters two texts share with a letter diff (Myers). The component
contract memory describes the model.

Files older than this keep :text-content-attribute and
:text-content-structure, read as every group they may cover until the
migration recomputes them.

Tests whose expected values the model changes: text-sync-test (the
copy's weight, letters or new run no longer keep the main's size out;
the letters read whole), text-touched-test (its edits went to the
paragraph node, which shows nothing; they now edit the run),
variants-switch-test (the copies with two runs keep their letters
across a switch, and the size override the mains agree on), the
editor output table, case Y and the notation test (new groups). The 1j
pins of F72-F79 and F81 go; F79's direct test of equal-attrs? becomes a
sync of editor content. A new check holds the production override rule
to the 1j reference rule. New tests: the letter view, and a detached,
duplicated or reset copy text keeping or clearing its override groups.

Benchmark (lower quantile, ms; baseline / now / ratio):
  deepest-edit-through-three-levels    35.2 /  36.6 / 1.04
  edit-through-a-chain-of-dependents   68.9 /  67.2 / 0.98
  library-update-into-copies           83.1 /  72.5 / 0.87
  main-edit-into-10-copies              1.8 /   1.5 / 0.82
  main-edit-into-100-copies            15.4 /  14.9 / 0.97
  main-edit-into-1000-copies          153.1 / 153.4 / 1.00
  variant-switch-into-copies           32.1 /  27.1 / 0.85
The worlds hold no text. A main text edit synced into 100 copy texts
that changed their letters takes 40 ms, against 29 ms before: the merge
aligns the letters of each copy with the main's.

AI-assisted-by: claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:22:21 +02:00

936 lines
51 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns common-tests.logic.text-sync-test
(: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.types.text :as txt]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
(t/deftest sync-unchanged-copy-when-changed-attribute
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(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)
main-child (ths/get-shape file :main-child)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "32" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest sync-unchanged-copy-when-changed-text
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(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)
main-child (ths/get-shape file :main-child)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
(t/is (= "Bye" (:text line)))))
(t/deftest sync-unchanged-copy-when-changed-both
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(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)
main-child (ths/get-shape file :main-child)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "32" (:font-size line)))
(t/is (= "Bye" (:text line)))))
(t/deftest sync-updated-attr-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-weight] "700"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; Each style is an override of its own: the copy's weight does not
;; keep the main's size out
(t/is (= "32" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest sync-updated-attr-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-weight] "700"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The text is updated because only attrs were touched
(t/is (= "Bye" (:text line)))))
(t/deftest sync-updated-attr-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-weight] "700"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; Each style is an override of its own: the copy's weight does not
;; keep the main's size out
(t/is (= "32" (:font-size line)))
;; The text is updated because only attrs were touched
(t/is (= "Bye" (:text line)))))
(t/deftest sync-updated-text-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Hi"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr is updated because only text were touched
(t/is (= "32" (:font-size line)))
(t/is (= "Hi" (:text line)))))
(t/deftest sync-updated-text-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Hi"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest sync-updated-text-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Hi"))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The attr is updated because only text were touched
(t/is (= "32" (:font-size line)))
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest sync-updated-both-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-weight] "700")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Hi")))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; Letters and a style overridden keep the other styles following the
;; main
(t/is (= "32" (:font-size line)))
(t/is (= "Hi" (:text line)))))
(t/deftest sync-updated-both-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-weight] "700")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Hi")))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest sync-updated-both-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-weight] "700")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Hi")))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; Letters and a style overridden keep the other styles following the
;; main
(t/is (= "32" (:font-size line)))
;; The text doesn't change, because it was touched
(t/is (= "Hi" (:text line)))))
(t/deftest sync-updated-structure-same-attrs-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (get-in shape [:content :children 0 :children 0 :children 0])]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
;; Update the attrs on all the content tree
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The style follows the main, as the copy only changed its letters
(t/is (= "32" (:font-size line)))
;; The copy keeps its letters
(t/is (= "hello worldhello world" (txt/content->text (:content copy-child'))))))
(t/deftest sync-updated-structure-same-attrs-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (get-in shape [:content :children 0 :children 0 :children 0])]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The copy keeps its letters
(t/is (= "hello worldhello world" (txt/content->text (:content copy-child'))))))
(t/deftest sync-updated-structure-same-attrs-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (get-in shape [:content :children 0 :children 0 :children 0])]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
;; Update the attrs on all the content tree
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The style follows the main, as the copy only changed its letters
(t/is (= "32" (:font-size line)))
;; The copy keeps its letters
(t/is (= "hello worldhello world" (txt/content->text (:content copy-child'))))))
(t/deftest sync-updated-structure-diff-attrs-copy-when-changed-attribute
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (-> (get-in shape [:content :children 0 :children 0 :children 0])
(assoc :font-weight "700"))]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
;; Update the attrs on all the content tree
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The copy's own letters and weight do not keep the main's size out
(t/is (= "32" (:font-size line)))
(t/is (= "hello world" (:text line)))))
(t/deftest sync-updated-structure-diff-attrs-copy-when-changed-text
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (-> (get-in shape [:content :children 0 :children 0 :children 0])
(assoc :font-weight "700"))]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :text] "Bye"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
(t/is (= "14" (:font-size line)))
;; The copy keeps its letters
(t/is (= "hello world" (:text line)))))
(t/deftest sync-updated-structure-diff-attrs-copy-when-changed-both
(let [;; ==== Setup
file0 (-> (thf/sample-file :file1)
(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]}))
page0 (thf/current-page file0)
copy-child (ths/get-shape file0 :copy-child)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page0))
#{(:id copy-child)}
(fn [shape]
(let [line (-> (get-in shape [:content :children 0 :children 0 :children 0])
(assoc :font-weight "700"))]
(update-in shape [:content :children 0 :children 0 :children]
#(conj % line))))
(:objects (thf/current-page file0))
{})
file (thf/apply-changes file0 changes)
main-child (ths/get-shape file :main-child)
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id main-child)}
(fn [shape]
;; Update the attrs on all the content tree
(-> shape
(assoc-in [:content :children 0 :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :font-size] "32")
(assoc-in [:content :children 0 :children 0 :children 0 :text] "Bye")))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; The copy's own letters and weight do not keep the main's size out
(t/is (= "32" (:font-size line)))
;; The copy keeps its letters
(t/is (= "hello world" (:text line)))))
(t/deftest sync-copy-touched-only-in-content-group
;; A copy text whose `:touched` has `:content-group` but no text subgroup,
;; as files from before the subgroups hold it, keeps its own content
;; when the main changes.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(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]}))
copy-child (ths/get-shape file :copy-child)
file (-> file
(ths/update-shape :copy-child :content
(assoc-in (:content copy-child)
[:children 0 :children 0 :children 0 :text]
"copy text"))
(ths/update-shape :copy-child :touched #{:content-group}))
page (thf/current-page file)
;; ==== Action
changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(thi/id :main-child)}
(fn [shape]
(assoc-in shape [:content :children 0 :children 0 :children 0 :font-size] "32"))
(:objects page)
{})
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
nil
:components
(:id updated-file)
(thi/id :component1)
(:id updated-file)
{(:id updated-file) updated-file}
(:id updated-file))
file' (thf/apply-changes updated-file changes2)
;; ==== Get
copy-child' (ths/get-shape file' :copy-child)
line (get-in copy-child' [:content :children 0 :children 0 :children 0])]
;; with no subgroup, sync takes the "nothing touched" branch and writes
;; the main's content
(tm/known-failure "F50" "copy text" "hello world" (:text line))
(t/is (= #{:content-group} (:touched copy-child')))))