penpot/frontend/test/frontend_tests/data/workspace_reflow_test.cljs
Alonso Torres cb21f6401a
Add waitForLayoutUpdate plugin method (#9898)
*  Add waitForLayoutUpdate plugin method

*  Refactor internal wait for tasks
2026-07-28 14:44:30 +02:00

160 lines
6.8 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 INC Sucursal en España SL
(ns frontend-tests.data.workspace-reflow-test
"Tests the reflow tasks the layout and text pipelines feed to
`app.main.data.workspace.reflow`, which is what plugin waits observe."
(:require
[app.common.uuid :as uuid]
[app.main.data.workspace.reflow :as wrf]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.texts :as dwtxt]
[cljs.test :as t :include-macros true]
[potok.v2.core :as ptk]))
(t/use-fixtures :each {:before wrf/reset-pending!
:after wrf/reset-pending!})
;; Starts the `:layout/update` pipeline on a throwaway store; its empty state
;; resolves every buffered update to no shapes and applies no modifiers.
(defn- start-pipeline!
[]
(doto (ptk/store {:state {} :on-error #(js/console.error %)})
(ptk/emit! (dwsl/initialize-shape-layout))))
(defn- stop-pipeline!
[store]
(ptk/emit! store (dwsl/finalize-shape-layout)))
(defn- start-text-pipeline!
[]
(doto (ptk/store {:state {} :on-error #(js/console.error %)})
(ptk/emit! (dwtxt/initialize-text-reflow))))
(defn- stop-text-pipeline!
[store]
(ptk/emit! store (dwtxt/finalize-text-reflow)))
(t/deftest root-only-layout-update-is-not-pending-work
;; The root lays out nothing, so an update with only its id holds no wait.
(t/async done
(let [store (start-pipeline!)]
(ptk/emit! store (ptk/data-event :layout/update {:ids [uuid/zero]}))
(-> (wrf/wait-for-layout-update nil 20)
(.then #(t/is true "resolved with no pending work"))
(.catch #(t/is false "a root-only update was marked as pending work"))
(.then (fn []
(stop-pipeline! store)
(done)))))))
(t/deftest stale-task-cannot-finish-current-work
;; Delayed callbacks from a finalized workspace carry the old generation and
;; must not drain a new workspace task for the same shape id.
(t/async done
(let [id (uuid/next)
stale-task (wrf/start! :text-measure [id])
_ (wrf/reset-pending!)
current-task (wrf/start! :text-measure [id])]
(wrf/finish! stale-task)
(-> (wrf/wait-for-layout-update [id] 20)
(.then #(t/is false "a stale completion drained current work"))
(.catch #(t/is true "current work stayed pending"))
(.then (fn []
(wrf/finish! current-task)
(wrf/wait-for-layout-update [id] 100)))
(.then #(t/is true "the exact current task drained normally"))
(.catch #(t/is false "the current task did not drain"))
(.then (fn [] (done)))))))
(t/deftest pending-promise-finishes-at-the-operation-boundary
;; Imperative render work is pending from before its thunk starts until the
;; exact promise returned by that thunk settles; no timer is involved.
(t/async done
(let [id (uuid/next)
resolve* (atom nil)
started? (atom false)]
(wrf/run-pending!
:text-measure
[id]
(fn []
(reset! started? true)
(js/Promise. (fn [resolve _] (reset! resolve* resolve)))))
(-> (wrf/wait-for-layout-update [id] 20)
(.then #(t/is false "resolved while the render operation was pending"))
(.catch #(t/is @started? "the task was opened before running the operation"))
(.then (fn []
(@resolve*)
(wrf/wait-for-layout-update [id] 100)))
(.then #(t/is true "resolved as soon as the render operation settled"))
(.catch #(t/is false "the settled render operation stayed pending"))
(.then (fn [] (done)))))))
(t/deftest pending-promise-finishes-after-a-synchronous-error
(let [id (uuid/next)]
(try
(wrf/run-pending! :text-measure [id] #(throw (js/Error. "boom")))
(catch :default _))
(t/async done
(-> (wrf/wait-for-layout-update [id] 100)
(.then #(t/is true "a synchronous failure drained its exact task"))
(.catch #(t/is false "a synchronous failure leaked pending work"))
(.then (fn [] (done)))))))
(t/deftest deleted-shapes-cancel-only-their-work
(t/async done
(let [id-a (uuid/next)
id-b (uuid/next)
task-a (wrf/start! :text-bridge [id-a])
task-b (wrf/start! :text-bridge [id-b])]
(wrf/cancel-shapes! [id-a])
(-> (wrf/wait-for-layout-update [id-a] 100)
(.then #(t/is true "deleted shape work was cancelled"))
(.catch #(t/is false "deleted shape work stayed pending"))
(.then #(wrf/wait-for-layout-update [id-b] 20))
(.then #(t/is false "cancelling one shape drained its sibling"))
(.catch #(t/is true "sibling work stayed pending"))
(.then (fn []
(wrf/finish! task-a)
(wrf/finish! task-b)
(done)))))))
(t/deftest text-bridge-waits-for-each-shape
;; Starting measurement for one id must not release sibling bridge tasks.
(t/async done
(let [store (start-text-pipeline!)
id-a (uuid/next)
id-b (uuid/next)]
(ptk/emit! store (ptk/data-event :text/reflow {:ids [id-a id-b]}))
(let [task-a (wrf/start! :text-measure [id-a])]
(wrf/finish! task-a))
(-> (wrf/wait-for-layout-update [id-b] 20)
(.then #(t/is false "the first text released its sibling bridge"))
(.catch #(t/is true "the sibling bridge stayed pending"))
(.then (fn []
(let [task-b (wrf/start! :text-measure [id-b])]
(wrf/finish! task-b))
(wrf/wait-for-layout-update [id-b] 100)))
(.then #(t/is true "the sibling drained after its own measurement"))
(.catch #(t/is false "the sibling never drained"))
(.then (fn []
(stop-text-pipeline! store)
(done)))))))
(t/deftest layout-update-is-pending-until-the-buffer-flushes
;; A shape id is marked on arrival and drained when the update is processed.
(t/async done
(let [store (start-pipeline!)]
(ptk/emit! store (ptk/data-event :layout/update {:ids [(uuid/next) uuid/zero]}))
(-> (wrf/wait-for-layout-update nil 20)
(.then #(t/is false "resolved while the update was still buffered"))
(.catch #(t/is true "stayed pending until the flush"))
(.then #(wrf/wait-for-layout-update nil 5000))
(.then #(t/is true "resolved once the update was processed"))
(.catch #(t/is false "the pipeline never drained its mark"))
(.then (fn []
(stop-pipeline! store)
(done)))))))