penpot/frontend/test/frontend_tests/data/workspace_notifications_test.cljs
alonso.torres 899f05114e 🐛 Fix small save bugs found by the rollback test plan
- Ending a drag inside the frame the shapes already belong to sent an
  empty reparent save and bumped the revision. The move commits only
  when some shape changes parent.
- When a user restored a version, other sessions reloaded the file with
  no notice. They now see a dialog naming who restored it.
- Exports drawn in the browser (PDF, SVG, and PNG when the WASM renderer
  draws it) and duplicating a file from the dashboard started at once,
  so an export could show changes a pending save then rejected, and a
  copy could miss queued edits. Both wait for the file's pending saves.
  A client export warns that it includes unsaved changes; a copy warns
  that it misses them.
2026-11-03 11:17:25 +01:00

114 lines
4.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.data.workspace-notifications-test
(:require
[app.common.data :as d]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.workspace :as-alias dw]
[app.main.data.workspace.notifications :as dwntf]
[app.util.i18n :as i18n]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.async :as async]
[potok.v2.core :as ptk]))
(defn- file-change-msg
[extra]
(merge {:type :file-change
:profile-id (uuid/next)
:file-id (uuid/next)
:session-id (uuid/next)
:revn 1
:vern 0
:changes []}
extra))
(defn- ^:async watch-file-change
[msg]
(let [events (atom [])]
(await (async/observe (ptk/watch (dwntf/handle-file-change msg) {} (rx/empty))
:on-next #(swap! events conj %)))
@events))
(t/deftest ^:async repaired-file-change-applies-and-notifies
(let [events (await (watch-file-change (file-change-msg {:repaired true})))
types (mapv ptk/type events)
show (d/seek #(= ::ntf/show (ptk/type %)) events)
state (ptk/update show {})]
(t/is (= [::dch/commit ::ntf/show] types))
(t/is (= :info (get-in state [:notification :level])))
(t/is (nil? (get-in state [:notification :timeout]))
"the toast stays until the user closes it")
(t/is (= (i18n/tr "workspace.notifications.file-repaired")
(get-in state [:notification :content])))))
(t/deftest ^:async ordinary-file-change-does-not-notify
(let [events (await (watch-file-change (file-change-msg {})))]
(t/is (= [::dch/commit] (mapv ptk/type events)))))
(defn- restored-events
"Events the watch of `handle-file-restored` emits for `msg` on `state`."
[state msg]
(let [events (atom [])]
(-> (async/observe (ptk/watch (dwntf/handle-file-restored msg) state nil)
:on-next #(swap! events conj %))
(.then (fn [_] @events)))))
(defn- shown-modal
[events]
(some #(when (= ::modal/show-modal (ptk/type %))
(::modal/modal (ptk/update % {})))
events))
;; Scenario: a peer restores an earlier version of the open file. The file
;; reloads and a modal names the peer. Proves: the reload is not silent.
(t/deftest ^:async a-peer-restore-reloads-and-names-the-peer
(let [file-id (uuid/next)
session-id (uuid/next)
profile-id (uuid/next)
state {:current-file-id file-id
:files {file-id {:id file-id :vern 1}}
:workspace-presence {session-id {:profile-id profile-id}}
:profiles {profile-id {:id profile-id :fullname "Ada"}}}
events (await (restored-events state {:type :file-restored
:file-id file-id
:session-id session-id
:vern 2}))
modal (shown-modal events)]
(t/is (some #(= ::dw/reload-current-file (ptk/type %)) events))
(t/is (= :file-restored (:type modal)))
(t/is (= "Ada" (get-in modal [:props :fullname])))))
;; Scenario: the restoring session left before its message arrived, so it
;; has no presence. Proves: the modal still shows, without a name.
(t/deftest ^:async a-restore-from-an-unknown-session-shows-no-name
(let [file-id (uuid/next)
state {:current-file-id file-id
:files {file-id {:id file-id :vern 1}}}
events (await (restored-events state {:type :file-restored
:file-id file-id
:session-id (uuid/next)
:vern 2}))
modal (shown-modal events)]
(t/is (= :file-restored (:type modal)))
(t/is (nil? (get-in modal [:props :fullname])))))
;; Scenario: the file already shows the restored version. Proves: no reload
;; and no modal for a version the user already sees.
(t/deftest ^:async a-restore-of-the-loaded-version-does-nothing
(let [file-id (uuid/next)
state {:current-file-id file-id
:files {file-id {:id file-id :vern 2}}}
events (await (restored-events state {:type :file-restored
:file-id file-id
:session-id (uuid/next)
:vern 2}))]
(t/is (empty? events))))