penpot/frontend/test/frontend_tests/composable_tests/completion_contract_test.cljs
alonso.torres 158a8e1dba ♻️ Tidy the composable suite and make its cases stricter
A review of the suite after the new operations:
- Dead helpers and duplicate nodes go; the lineage reset and swap
  nodes are constructors of reset and swap-shape.
- Repeated helpers are shared: grid boards (tho/add-grid-frame,
  tho/place-in-grid-columns), n/nested-head-under, n/child-shape,
  r/sync-cascade (the benchmark uses it too), the frontend fixture and
  fill helpers, and the plugin getMainComponent.
- Runner tests that repeated cases B, C and D go.
- Comments say what the code does, and the wrong ones are fixed.
- Case K's remote edit painted red on red and its unedited variants
  asserted nothing; it starts grey and every variant asserts.
- F43 is pinned in one case, AJ, by the value one undo leaves. L, M, Q
  and K get real frame sizes, since their pins depended on when the
  100 ms layout buffer flushed.
- make-nested-component takes a level count and depth-sweep gives each
  depth once, so K, Q, R, T and W stop running depth 1 twice.
- The completion-contract tests no longer depend on fixed pauses or
  on how the watcher batches its events.

Benchmark against the baseline (lower quantile): ratios 0.92 to 1.03,
no world flagged.
2026-10-16 11:05:20 +02:00

161 lines
6.7 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.composable-tests.completion-contract-test
"Tests the interpreter's completion contract: a step resolves only once the
work it caused has drained, whatever kind of commit it makes, and reports
the pending work when it does not drain in time. Component sync is slowed
down on purpose, so a step that resolved early would see a half-synced
file."
(:require
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.comp.setups :as setup]
[app.common.test-helpers.composable.comp.undo-check :as uc]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.reflow :as wrf]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.undo :as dwu]
[app.main.store :as st]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.composable-tests.interpreter :as ftm]
[frontend-tests.helpers.mock :as mock]
[potok.v2.core :as ptk]))
(t/use-fixtures :each ftm/fixture)
(def ^:private original "#abcdef")
(def ^:private red "#ff0000")
;; How much later than asked the slowed-down sync starts.
(def ^:private sync-delay-ms 150)
(defn- deferred-sync-file
"A replacement for `dwl/sync-file` that holds its sync wait open and runs
`work` on the event that `sync-file`, the real constructor, builds."
[sync-file work]
(mock/stub
(fn [& args]
(let [event (apply sync-file args)
file-id (first args)]
(ptk/reify ::deferred-sync-file
ptk/WatchEvent
(watch [_ _ _]
(wrf/with-pending :sync-file [file-id] (work event))))))))
(defn- slow-sync
[event]
(->> (rx/of event)
(rx/delay sync-delay-ms)))
(defn- endless-sync
[_]
(rx/create (fn [_] nil)))
(defn- fill-main-child
[color]
(dwsh/update-shapes #{(thi/id :main-child)}
#(assoc % :fills (ths/sample-fills-color :fill-color color))))
(t/deftest ^:async edit-step-waits-for-slow-sync
(let [sync-file dwl/sync-file]
(await
(mock/with-mocks*
{dwl/sync-file (deferred-sync-file sync-file slow-sync)}
(ftm/install! (setup/simple-component-with-labeled-copy))
(t/is (= :settled (await (ftm/await-step [(fill-main-child red)]))))
(t/is (= red (ftm/fill-of :copy-child)))))))
(t/deftest ^:async undo-and-redo-steps-follow-a-slow-sync
;; Undo must find the slow sync already committed: it reverts the edit and
;; its sync together only when both are on the undo stack.
(let [sync-file dwl/sync-file]
(await
(mock/with-mocks*
{dwl/sync-file (deferred-sync-file sync-file slow-sync)}
(ftm/install! (setup/simple-component-with-labeled-copy))
(await (ftm/await-step [(fill-main-child red)]))
(t/is (= red (ftm/fill-of :copy-child)) "the edit step ends synced")
(t/is (= :settled (await (ftm/await-step [dwu/undo]))))
(t/is (= original (ftm/fill-of :main-child)))
(t/is (= original (ftm/fill-of :copy-child)))
(t/is (= :settled (await (ftm/await-step [dwu/redo]))))
(t/is (= red (ftm/fill-of :main-child)))
(t/is (= red (ftm/fill-of :copy-child)))))))
(defn- watch-touches
"Collect the ids of the components that `touch-component` events touch from
now on. Returns `[ids-atom subscription]`."
[]
(let [touched (atom #{})
sub (rx/sub! (rx/filter (ptk/type? ::dwl/touch-component) st/stream)
(fn [event] (swap! touched into @event)))]
[touched sub]))
(t/deftest ^:async undo-and-redo-steps-wait-for-the-watcher
;; Undo and redo start no sync: the watcher only touches the component, a
;; timer turn after the commit. A step that resolved on the commit alone
;; would finish before that touch.
(ftm/install! (setup/simple-component-with-labeled-copy))
(await (ftm/await-step [(fill-main-child red)]))
(let [component #{(thi/id :component1)}
[touched sub] (watch-touches)]
(t/is (= :settled (await (ftm/await-step [dwu/undo]))))
(t/is (= component @touched) "the undo step waited for the watcher")
(reset! touched #{})
(t/is (= :settled (await (ftm/await-step [dwu/redo]))))
(t/is (= component @touched) "the redo step waited for the watcher")
(rx/dispose! sub)))
(t/deftest ^:async dropped-commit-steps-settle
;; Translation and derived commits start no sync; their steps must still
;; resolve.
(ftm/install! (setup/simple-component-with-labeled-copy))
(t/is (= :settled (await (ftm/await-step [(ftm/fill-commit :main-child red {:translation? true})]))))
(t/is (= :settled (await (ftm/await-step [(ftm/fill-commit :main-child red {:skip-component-sync? true})])))))
(defn- run-endless-step
"Install a component whose sync never finishes and run a main edit step with
a short timeout. Resolves with the step result and the file id."
[]
(let [sync-file dwl/sync-file]
(mock/with-mocks*
{dwl/sync-file (deferred-sync-file sync-file endless-sync)}
(ftm/install! (setup/simple-component-with-labeled-copy))
(-> (ftm/await-step [(fill-main-child red)] 100)
(.then (fn [result] [result (:id (ftm/current-file))]))))))
(t/deftest ^:async step-that-never-settles-reports-the-pending-work
(let [[result file-id] (await (run-endless-step))]
(t/is (contains? (get-in result [:timeout file-id]) :sync-file))))
(t/deftest ^:async timed-out-step-does-not-hold-up-the-next-variant
;; The endless sync stays pending; installing the next variant forgets it.
(await (run-endless-step))
(ftm/install! (setup/simple-component-with-labeled-copy))
(t/is (empty? (wrf/pending)))
(t/is (= :settled (await (ftm/await-step [(fill-main-child red)]))))
(t/is (= red (ftm/fill-of :copy-child))))
(t/deftest skipped-optional-keeps-the-undo-baseline
;; The end-of-variant round trip undoes back to the last file install. A
;; skipped optional installs nothing, so the baseline stays at the setup,
;; before the edit, where the undo index is -1.
(t/async done
(let [later (n/change-property :copy-instance :fills red)]
(ftm/check done
{:setup setup/simple-component-with-labeled-copy
:operation (tm/in-sequence [(n/change-property :main-instance :fills red)
(tm/optional later)])}
(fn [situation]
(when-not (tm/applied? situation later)
(t/is (= -1 (:depth (uc/baseline situation))))))))))