penpot/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs
Eva Marco c9146a30ab
🐛 Keep layer search and tree scroll across sidebar tab switches (#12117)
Switching sidebar tabs unmounts the layers panel. The search text and
filters lived only in component state, so on return the still open
search bar was treated as a new search and cleared. Keep that state per
file in a session atom, as the assets panel already does.

Closing or clearing the search mounts the tree container again, but the
restore hook only ran on mount or page change, so the tree went back to
the top. Let use-restore-scroll take an active? flag so each list
restores its own position whenever it is shown again, and drop the
saved search position on close so a new search starts at the top.

Closes #11972

AI-assisted-by: claude-opus-5-5
2026-10-08 11:45:35 +02:00

94 lines
3.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.ui.sidebar-scroll-test
(:require
[app.main.ui.workspace.sidebar.scroll :as sc]
[app.util.timers :as tm]
[cljs.test :as t :include-macros true]))
(def ^:private restore-loop! @#'sc/restore-loop!)
(def ^:private restore-position! @#'sc/restore-position!)
(defn- scroll-event
[scroll-top]
#js {:target #js {:scrollTop scroll-top}})
(t/deftest save-scroll-stores-position-per-key
(let [store (atom {})]
(sc/save-scroll! store [:layers :page-1] (scroll-event 250))
(sc/save-scroll! store [:assets :file-1] (scroll-event 80))
(t/is (= {[:layers :page-1] 250
[:assets :file-1] 80}
@store))))
(t/deftest save-scroll-ignores-events-without-target
(let [store (atom {})]
(sc/save-scroll! store [:layers :page-1] nil)
(sc/save-scroll! store [:layers :page-1] #js {})
(t/is (= {} @store))))
(t/deftest forget-scroll-drops-only-that-key
(let [store (atom {[:layers :page-1] 250
[:layers-search :page-1] 90})]
(sc/forget-scroll! store [:layers-search :page-1])
(t/is (= {[:layers :page-1] 250} @store))
(sc/forget-scroll! store [:layers-search :page-1])
(t/is (= {[:layers :page-1] 250} @store))))
(t/deftest restore-scroll-writes-saved-position
(let [node #js {:scrollTop 0}]
(sc/restore-scroll! node 320)
(t/is (= 320 (.-scrollTop node)))))
(t/deftest needs-restore-retry
(t/is (true? (sc/needs-restore-retry? -1 300 0)))
(t/is (true? (sc/needs-restore-retry? 100 300 9)))
(t/is (false? (sc/needs-restore-retry? 300 300 0)))
(t/is (false? (sc/needs-restore-retry? 100 300 10))))
(t/deftest restore-loop-writes-once-when-content-settled
(let [node #js {:scrollTop 0 :scrollHeight 300}
scheduled (atom [])
raf* (volatile! nil)]
(with-redefs [tm/raf (fn [f] (swap! scheduled conj f) 1)]
(restore-loop! node 250 300 0 raf*)
(t/is (= 250 (.-scrollTop node)))
(t/is (empty? @scheduled) "no follow-up when height is stable")
(t/is (nil? @raf*)))))
(t/deftest restore-loop-retries-while-content-grows
(let [node #js {:scrollTop 0 :scrollHeight 300}
scheduled (atom [])
raf* (volatile! nil)]
(with-redefs [tm/raf (fn [f] (swap! scheduled conj f) 1)]
(restore-loop! node 250 -1 0 raf*)
(t/is (= 250 (.-scrollTop node)))
(t/is (= 1 (count @scheduled)) "follow-up scheduled while growing")
(t/is (= 1 @raf*) "frame id tracked for cancellation")
;; Next frame: content settled at the same height, retry stops.
((first @scheduled))
(t/is (= 250 (.-scrollTop node)))
(t/is (= 1 (count @scheduled)) "no further frames once settled"))))
(t/deftest restore-position-without-saved-value-goes-to-top
(let [node #js {:scrollTop 400 :scrollHeight 900}
scheduled (atom [])
raf* (volatile! nil)]
(with-redefs [tm/raf (fn [f] (swap! scheduled conj f) 1)]
(restore-position! node nil raf*)
(t/is (= 0 (.-scrollTop node)) "a node kept mounted drops the old position")
(t/is (empty? @scheduled) "no retry loop for the top"))))
(t/deftest restore-position-with-saved-value-runs-loop
(let [node #js {:scrollTop 0 :scrollHeight 900}
scheduled (atom [])
raf* (volatile! nil)]
(with-redefs [tm/raf (fn [f] (swap! scheduled conj f) 1)]
(restore-position! node 250 raf*)
(t/is (= 250 (.-scrollTop node)))
(t/is (= 1 (count @scheduled)) "first pass schedules a settle check"))))