penpot/frontend/test/frontend_tests/data/workspace_versions_test.cljs
2026-09-15 19:22:56 +02:00

190 lines
9.5 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-versions-test
(:require
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.persistence :as dps]
[app.main.data.workspace :as dw]
[app.main.data.workspace.pages :as dwpg]
[app.main.data.workspace.versions :as versions]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.util.timers :as tm]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]
[potok.v2.core :as ptk]))
(t/deftest version-creation-rejects-save-failure-and-timeout
(doseq [failure [:error :timeout]
operation [:create :plugin-create]]
(t/testing (str operation " after " failure)
(let [file-id (uuid/next)
resolved (atom [])
rejected (atom [])
requests (atom [])
state {:current-file-id file-id}
event (case operation
:create (versions/create-version)
:plugin-create (versions/create-version-from-plugins
file-id "Version"
#(swap! resolved conj %) #(swap! rejected conj %)))]
(with-redefs [refs/persistence (atom {:status (if (= failure :error) :error :saving)
:error {:type :persistence :code :save-permission-denied}})
rp/cmd! (mock/stub (fn [cmd _]
(swap! requests conj cmd)
(rx/of nil)))
rx/timeout (mock/stub (fn [_ fallback source]
(if (= failure :timeout) fallback source)))]
(->> (ptk/watch event state (rx/empty))
(rx/subs! (fn [_]) #(swap! rejected conj %)))
(t/is (empty? @requests) "Do not create a version without saving")
(t/is (empty? @resolved) "Do not resolve the plugin promise on failure")
(t/is (= 1 (count @rejected)))
(t/is (= (if (= failure :error) :save-permission-denied :save-timeout)
(:code (ex-data (first @rejected))))))))))
(t/deftest leaving-the-workspace-clears-the-preview-flags
(let [team-id (uuid/next)
file-id (uuid/next)
other-id (uuid/next)
output (atom [])
state (-> {:current-file-id file-id
:current-team-id team-id
:permissions {:can-edit true}
:files {file-id {:id file-id :revn 0 :vern 0}}
:workspace-global {:read-only? true :preview-id (uuid/next)}}
(as-> $ (ptk/update (dw/finalize-workspace team-id file-id) $))
(assoc :current-file-id other-id)
(assoc-in [:files other-id] {:id other-id :revn 0 :vern 0}))]
(t/is (nil? (get-in state [:workspace-global :preview-id])))
(when-let [result (ptk/watch (dch/commit-changes {:redo-changes [] :undo-changes []})
state (rx/empty))]
(->> result (rx/subs! #(swap! output conj %))))
(t/is (= 1 (count @output))
"Editing must work again on the next file opened in the tab")))
(t/deftest strict-persistence-wait-rejects-a-real-timeout
(t/async done
(mock/with-mocks
{refs/persistence (atom {:status :saving})}
(fn [finish]
(->> (dps/wait-persisted-or-error 0)
(rx/subs! (fn [_]
(t/is false "The stalled save must not succeed")
(finish))
(fn [cause]
(t/is (= :save-timeout (:code (ex-data cause))))
(finish)))))
done)))
(t/deftest strict-persistence-wait-observes-a-later-failure
(let [pstate (atom {:status :saving})
rejected (atom [])
resolved (atom [])]
(with-redefs [refs/persistence pstate]
(let [subscription (->> (dps/wait-persisted-or-error)
(rx/subs! #(swap! resolved conj %) #(swap! rejected conj %)))]
(try
(reset! pstate {:status :error :error {:code :missing-commit}})
(t/is (empty? @resolved))
(t/is (= :missing-commit (:code (ex-data (first @rejected)))))
(finally
(rx/dispose! subscription)))))))
(t/deftest failed-restore-returns-to-the-live-file-before-reporting
;; The plugin promise is rejected on a later tick, so every assertion about
;; the rejection runs once the event loop has drained.
(t/async done
(let [deferred (atom [])]
(doseq [operation [:ui :plugin]
failure [:saving :timeout :restore]
preview? [true false]]
(let [label (str operation " after " failure ", preview=" preview?)
file-id (uuid/next)
version-id (uuid/next)
backup {:id file-id :revn 10}
snapshot {:id file-id :revn 5}
rejected (atom [])
resolved (atom [])
requests (atom [])
initialized (atom [])
at-reject (atom nil)
store (ptk/store {:state (cond-> {:current-file-id file-id
:files {file-id backup}}
preview?
(assoc :files {file-id snapshot}
:workspace-global {:read-only? true :preview-id version-id}
:workspace-versions {:backup backup}))
:on-error #(swap! rejected conj %)})
reject (fn [cause]
(reset! at-reject
{:file (get-in @store [:files file-id])
:preview-id (get-in @store [:workspace-global :preview-id])})
(swap! rejected conj cause))
event (if (= operation :ui)
(#'versions/restore-version version-id)
(versions/restore-version-from-plugin
file-id version-id #(swap! resolved conj %) reject))]
(t/testing label
(with-redefs [refs/persistence (atom {:status (case failure
:saving :error :timeout :saving :saved)})
rp/cmd! (mock/stub (fn [cmd _]
(swap! requests conj cmd)
(rx/throw (ex-info "Restore failed" {:type :internal}))))
rx/timeout (mock/stub (fn [_ fallback source]
(if (= failure :timeout) fallback source)))
dwpg/initialize-page (mock/stub
(fn [_ _]
(fn [state]
(swap! initialized conj (get-in state [:files file-id]))
state)))]
(ptk/emit! store event)
(t/is (empty? @resolved))
(t/is (= (if (= failure :restore) [:restore-file-snapshot] []) @requests)
"Do not restore a version without saving")
(t/is (= backup (get-in @store [:files file-id])))
(t/is (nil? (get-in @store [:workspace-global :read-only?])))
(t/is (nil? (get-in @store [:workspace-global :preview-id])))
(t/is (nil? (get-in @store [:workspace-versions :backup])))
(t/is (= (if preview? [backup] []) @initialized))))
(swap! deferred conj
(fn []
(t/testing label
(t/is (= 1 (count @rejected)))
(when (= operation :plugin)
(t/is (= {:file backup :preview-id nil} @at-reject)
"The live file is back before the plugin promise rejects")))
(rx/dispose! store)))))
(tm/schedule 50 (fn []
(doseq [check @deferred] (check))
(done))))))
(t/deftest successful-restore-clears-preview-only-after-loading
(let [file-id (uuid/next)
state {:current-file-id file-id
:current-team-id (uuid/next)
:current-page-id (uuid/next)
:workspace-global {:read-only? true :preview-id (uuid/next)}
:workspace-versions {:backup {:id file-id}}}
stream (rx/subject)
events (atom [])
cleanup #(filter (ptk/type? ::versions/clear-preview-state) @events)
sub (->> (ptk/watch (#'versions/initialize-version) state stream)
(rx/subs! #(swap! events conj %)))]
(try
(t/is (empty? (cleanup)))
(rx/push! stream (ptk/data-event ::dw/bundle-fetched nil))
(t/is (= 1 (count (cleanup))))
(let [result (ptk/update (first (cleanup)) state)]
(t/is (nil? (get-in result [:workspace-global :read-only?])))
(t/is (nil? (get-in result [:workspace-global :preview-id])))
(t/is (nil? (get-in result [:workspace-versions :backup]))))
(finally
(rx/dispose! sub)))))