alonso.torres 94c05589cf 🐛 Keep the UI in step with a rollback
A rollback can remove or change shapes the UI still refers to.

- The selection drops shapes that are gone and the color picker that
  edited them closes.
- The undo history is cut at the reverted action; it stays when no
  entry holds a reverted change. Undo and redo commits carry real undo
  changes and the history index they moved from, so a reverted Ctrl+Z
  puts the change and the index back.
- An open path edition whose shape the rollback changed closes without
  saving the editor's stale copy. The v2 text editor loads the reverted
  text again; the WASM editor already showed it.
- The Alt measurements look the hovered shape up in the current data.
- Plugin setters, resize() and rotate() on a removed shape throw
  "Shape not found", whatever the plugin's throwValidationErrors flag.
2026-11-05 23:59:32 +01:00

182 lines
6.9 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.helpers.persistence
"Fixture and builders shared by the persistence tests: a store with
persistence initialized over a mocked transport, synthetic commits, and
one-page file data to roll back."
(:require
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.persistence :as dps]
[app.main.errors :as errors]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.worker :as mw]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]
[potok.v2.core :as ptk]))
(defn local-commit
"Builds a synthetic local commit event for `file-id`: a page edit as redo,
no undo, from the `:local` source."
[file-id]
(ptk/data-event ::dch/commit
{:id (uuid/next)
:file-id file-id
:file-revn 0
:file-vern 0
:source :local
:features #{}
:redo-changes [{:type :mod-page :id (uuid/next) :name "Edited"}]
:undo-changes []}))
(defn with-persistence
"Async fixture for the persistence tests: mocks the transport, flash, the
worker and the leave-page task set (`:tasks`), and runs `f` with
persistence initialized.
Evaluates to a promise resolving once `f` settles and teardown completes;
`await` it from an `^:async` test.
By default the transport responds through the `:response` subject. Pass a
`respond` function (`(fn [cmd params] observable)`) to drive the transport
response directly instead. Delivery through the subject is asynchronous
(`observe-on :async`): await each effect — via `wait-for` — before
asserting it."
[f & [respond]]
(let [file-id (uuid/next)
response (rx/subject)
failures (atom [])
requests (atom [])
tasks (atom #{})
store (ptk/store {:state {:current-file-id file-id
:permissions {:can-edit true}
:files {file-id {:id file-id :revn 0}}}
:on-error #(t/is false (str %))})]
(mock/with-mocks*
{rp/cmd! (mock/stub (fn [cmd params]
(swap! requests conj [cmd params])
(if respond
(respond cmd params)
(->> response (rx/take 1) (rx/observe-on :async)))))
errors/flash (fn [& {:keys [cause]}]
(swap! failures conj cause))
;; Index updates of applied changes go to a worker the tests lack
mw/ask! (mock/stub (fn [_] (rx/empty)))
st/ongoing-tasks tasks}
(try
(ptk/emit! store (dps/initialize-persistence))
(await (f {:file-id file-id :response response :failures failures
:requests requests :store store :tasks tasks}))
(finally
(rx/dispose! store)
(rx/end! response))))))
(defn set-attr
"A change that sets `attr` of shape `shape-id` to `val`."
[page-id shape-id attr val]
{:type :mod-obj :id shape-id :page-id page-id
:operations [{:type :set :attr attr :val val}]})
(defn rename
"A change that renames shape `shape-id` to `name`."
[page-id shape-id name]
(set-attr page-id shape-id :name name))
(defn attr-commit
"A local commit of `file-id` that sets `attr` of the shape from `from` to
`to`, with its own undo group."
[file-id page-id shape-id attr from to]
(ptk/data-event ::dch/commit
{:id (uuid/next)
:file-id file-id
:file-revn 0
:file-vern 0
:source :local
:features #{}
:undo-group (uuid/next)
:redo-changes [(set-attr page-id shape-id attr to)]
:undo-changes [(set-attr page-id shape-id attr from)]}))
(defn rename-commit
"A local commit of `file-id` that renames the shape from `from` to `to`,
with its own undo group."
[file-id page-id shape-id from to]
(attr-commit file-id page-id shape-id :name from to))
(defn remote-commit
"A peer's commit of `changes` to `file-id` at revision `revn`."
[file-id revn changes]
(dch/commit {:file-id file-id
:file-revn revn
:file-vern 0
:source :remote
:save-undo? false
:redo-changes changes
:undo-changes []}))
(defn file-data
"File data with one page holding one rectangle named `name`, with `attrs`
on top of its defaults."
[page-id shape-id name & {:as attrs}]
{:pages [page-id]
:pages-index {page-id {:id page-id
:name "Page"
:objects {uuid/zero (cts/setup-shape {:id uuid/zero :type :frame :shapes [shape-id]})
shape-id (merge (cts/setup-shape {:id shape-id :type :rect :name name
:parent-id uuid/zero :frame-id uuid/zero})
attrs)}}}})
(defn add-shape
"`data` with `shape` added as the last child of the root of `page-id`."
[data page-id shape]
(-> data
(assoc-in [:pages-index page-id :objects (:id shape)] shape)
(update-in [:pages-index page-id :objects uuid/zero :shapes] conj (:id shape))))
(defn open-file
"A state update that opens `page-id` of `file-id` with `data` loaded."
[file-id page-id data]
#(-> %
(assoc :current-page-id page-id)
(assoc-in [:files file-id :data] data)))
(defn open-file-with-library
"`open-file` that also loads library `lib-id` with `lib-data`."
[file-id lib-id page-id data lib-data]
(comp #(assoc-in % [:files lib-id] {:id lib-id :revn 0 :data lib-data})
(open-file file-id page-id data)))
(defn shape-attr
[store file-id page-id shape-id attr]
(get-in @store [:files file-id :data :pages-index page-id :objects shape-id attr]))
(defn shape-name
[store file-id page-id shape-id]
(shape-attr store file-id page-id shape-id :name))
(defn undo-entry
"The undo history entry `commit` would add."
[commit]
(let [{:keys [undo-group undo-changes redo-changes]} @commit]
{:undo-group undo-group
:undo-groups #{undo-group}
:undo-changes undo-changes
:redo-changes redo-changes
:tags #{}}))
(defn rejection
"A save answer that rejects `commit-ids`, the first one failing
validation."
[& commit-ids]
{:revn 0
:rejected {:commit-ids (vec commit-ids)
:failed-commit-id (first commit-ids)
:error {:type :validation :code :referential-integrity}}})