penpot/frontend/test/frontend_tests/data/workspace_focus_test.cljs
alonso.torres d019804fd4 🐛 Keep comments, focus, thumbnails and text in step with a rollback
A rollback reverts the local data with a rebase, which is not a commit,
so the parts of the workspace that follow commits missed it:

- Comment pins moved in the reverted action stayed where they were, on
  screen and on the server, and pins on a board the rollback removed
  kept pointing to it. They go back to the reverted position, and pins
  on a removed board move to the page root.
- Focus mode stayed on with no shape left when the rollback removed the
  focused ones, and the next shape drawn turned it on again. It ends
  when its last shape goes, and the selection drops removed shapes.
- Component thumbnails kept the rejected look. The thumbnails of the
  reverted mains are rendered and uploaded again, and an upload whose
  shapes changed during the render is skipped.
- Under WASM, an open text editor reset to the reverted content but the
  canvas kept the old layout. It is laid out again.
- A library sync whose save failed showed no update notice again once
  the release undid it. The notice is checked again after that
  rollback.
2026-11-05 10:56:46 +01:00

88 lines
4.1 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.data.workspace-focus-test
"Tests for focus mode when the focused shapes go away."
(:require
[app.common.data :as d]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.main.data.workspace.selection :as dws]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.async :as async]
[potok.v2.core :as ptk]))
(defn- focus-store
"A store on a page holding `ids`, in focus mode on `focused`, entered
with `selected` selected."
[ids focused selected]
(let [file-id (uuid/next)
page-id (uuid/next)
objects (into {uuid/zero (cts/setup-shape {:id uuid/zero :type :frame :shapes (vec ids)})}
(map (fn [id]
[id (cts/setup-shape {:id id :type :rect :name "Rect"
:parent-id uuid/zero :frame-id uuid/zero})]))
ids)
local {:selected (into (d/ordered-set) selected) :zoom 1}]
(ptk/store {:state {:current-file-id file-id
:current-page-id page-id
:files {file-id {:id file-id
:data {:pages [page-id]
:pages-index {page-id {:id page-id :objects objects}}}}}
:workspace-local (assoc local :selected (d/ordered-set))
:workspace-pre-focus local
:workspace-focus-selected (set focused)}
:on-error #(t/is false (str %))})))
(defn- remove-shape
"Takes `id` out of the page of `store`, as a delete or a rollback does."
[store id]
(let [{:keys [current-file-id current-page-id]} @store]
(ptk/emit! store #(update-in % [:files current-file-id :data :pages-index current-page-id :objects]
dissoc id))))
;; Scenario: focus mode is on R, entered with R selected, and R goes away
;; (a rollback or a delete); later a new shape is drawn. Proves: focus mode
;; ends when its last shape goes, the selection from before it comes back
;; without R, and the new shape does not bring focus mode back.
(t/deftest ^:async focus-mode-ends-when-its-last-shape-is-removed
(let [[r s n] (repeatedly 3 uuid/next)
store (focus-store [r s] [r] [r])
left (atom 0)
sub (->> (ptk/input-stream store)
(rx/filter (ptk/type? ::dws/focus-mode-left))
(rx/subs! (fn [_] (swap! left inc))))]
(remove-shape store r)
(ptk/emit! store (dws/update-focus-shapes #{} #{r}))
(await (async/settle))
(t/is (nil? (:workspace-focus-selected @store)) "focus mode is off")
(t/is (nil? (:workspace-pre-focus @store)))
(t/is (empty? (get-in @store [:workspace-local :selected]))
"the removed shape is not selected again")
(t/is (= 1 (:zoom (:workspace-local @store))) "the viewport from before comes back")
(ptk/emit! store (dws/update-focus-shapes #{n} #{}))
(await (async/settle))
(t/is (nil? (:workspace-focus-selected @store)) "a new shape does not turn it on")
(t/is (= 1 @left) "the focus watcher is told to stop once")
(rx/dispose! sub)))
;; Scenario: focus mode is on R and S, entered with both selected; R goes
;; away and the user leaves focus mode with F. Proves: focus mode stays on
;; while a focused shape is left, and leaving it selects only the shapes
;; that still exist.
(t/deftest ^:async leaving-focus-mode-selects-only-shapes-that-exist
(let [[r s] (repeatedly 2 uuid/next)
store (focus-store [r s] [r s] [r s])]
(remove-shape store r)
(ptk/emit! store (dws/update-focus-shapes #{} #{r}))
(await (async/settle))
(t/is (= #{s} (:workspace-focus-selected @store)))
(ptk/emit! store (dws/toggle-focus-mode))
(await (async/settle))
(t/is (nil? (:workspace-focus-selected @store)))
(t/is (= [s] (vec (get-in @store [:workspace-local :selected]))))))