From 46f5346045f29214eac49732fb4bd6b129ae2aff Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Tue, 30 Jun 2026 14:37:27 +0200 Subject: [PATCH] :recycle: Merge :thumbnails and :thumbnails-meta into single state key (#10021) * :recycle: Merge :thumbnails and :thumbnails-meta into single state key :recycle: Unify thumbnail refs in a single ref :bug: Fix test * :recycle: Update tests --- frontend/src/app/main/data/workspace.cljs | 2 +- .../app/main/data/workspace/libraries.cljs | 6 +- .../app/main/data/workspace/thumbnails.cljs | 14 +- .../main/data/workspace/thumbnails_wasm.cljs | 2 +- frontend/src/app/main/refs.cljs | 11 +- .../app/main/ui/workspace/shapes/frame.cljs | 9 +- .../ui/workspace/sidebar/assets/common.cljs | 19 +- .../ui/workspace/viewport/interactions.cljs | 2 +- .../data/workspace_thumbnails_test.cljs | 491 +++++++++--------- .../test/frontend_tests/helpers/mock.cljc | 15 +- 10 files changed, 292 insertions(+), 279 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 39b28fbdc0..5a2d4446c6 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -275,7 +275,7 @@ ptk/UpdateEvent (update [_ state] (-> state - (assoc :thumbnails thumbnails) + (assoc :thumbnails (d/update-vals thumbnails (fn [uri] {:uri uri :rendered-at nil}))) (update :files assoc file-id file))))) (defn zoom-to-frame diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index a3a93cf7cc..5225f5cecf 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -800,7 +800,8 @@ (ptk/reify ::library-thumbnails-fetched ptk/UpdateEvent (update [_ state] - (update state :thumbnails merge thumbnails)))) + (update state :thumbnails merge + (d/update-vals thumbnails (fn [uri] {:uri uri :rendered-at nil})))))) (defn fetch-library-thumbnails [library-id] @@ -1541,7 +1542,8 @@ (->> (rp/cmd! :get-file-object-thumbnails {:file-id library-id :tag "component"}) (rx/map (fn [thumbnails] (fn [state] - (update state :thumbnails merge thumbnails)))))))))) + (update state :thumbnails merge + (d/update-vals thumbnails (fn [uri] {:uri uri :rendered-at nil})))))))))))) (defn link-file-to-library [file-id library-id] diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 3770a24a4a..3526c1ed99 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -134,12 +134,11 @@ ptk/UpdateEvent (update [_ state] - (let [uri (dm/get-in state [:thumbnails object-id])] + (let [uri (dm/get-in state [:thumbnails object-id :uri])] (l/dbg :hint "clear-thumbnail" :object-id object-id :uri uri) (-> state (update ::thumbnails-deletion-queue assoc object-id uri) - (update :thumbnails dissoc object-id) - (update :thumbnails-meta dissoc object-id)))) + (update :thumbnails dissoc object-id)))) ptk/WatchEvent (watch [_ _ stream] @@ -156,13 +155,12 @@ (ptk/reify ::assoc-thumbnail ptk/UpdateEvent (update [_ state] - (let [prev-uri (dm/get-in state [:thumbnails object-id]) - now (.now js/Date)] - (some->> prev-uri (vreset! prev-uri*)) + (let [prev-entry (dm/get-in state [:thumbnails object-id]) + now (ct/now)] + (some->> prev-entry :uri (vreset! prev-uri*)) (l/trc :hint "assoc thumbnail" :object-id object-id :uri uri) (-> state - (update :thumbnails assoc object-id uri) - (update :thumbnails-meta assoc object-id {:rendered-at now})))) + (update :thumbnails assoc object-id {:uri uri :rendered-at now})))) ptk/EffectEvent (effect [_ _ _] diff --git a/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs b/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs index 612bdd2e53..7d478c6448 100644 --- a/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs @@ -168,7 +168,7 @@ (ptk/reify ::persist-thumbnail ptk/WatchEvent (watch [_ state _] - (let [data-uri (dm/get-in state [:thumbnails object-id])] + (let [data-uri (dm/get-in state [:thumbnails object-id :uri])] (if (and (some? data-uri) (str/starts-with? data-uri "data:")) (let [blob (wapi/data-uri->blob data-uri)] diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index fc14e846f1..cf6e9e17f9 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -580,14 +580,9 @@ [object-id] (l/derived (fn [state] - (some-> (dm/get-in state [:thumbnails object-id]) - (cf/resolve-media))) - st/state)) - -(defn workspace-thumbnail-rendered-at - [object-id] - (l/derived - #(dm/get-in % [:thumbnails-meta object-id :rendered-at]) + (when-let [entry (dm/get-in state [:thumbnails object-id])] + (cond-> entry + (:uri entry) (update :uri cf/resolve-media)))) st/state)) (def workspace-text-modifier diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 2b09050dfe..27346b3178 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -136,10 +136,11 @@ width (dm/get-prop bounds :width) height (dm/get-prop bounds :height) - thumbnail-uri* (mf/with-memo [file-id page-id frame-id] - (let [object-id (thc/fmt-object-id file-id page-id frame-id "frame")] - (refs/workspace-thumbnail-by-id object-id))) - thumbnail-uri (mf/deref thumbnail-uri*) + thumbnail-data* (mf/with-memo [file-id page-id frame-id] + (let [object-id (thc/fmt-object-id file-id page-id frame-id "frame")] + (refs/workspace-thumbnail-by-id object-id))) + thumbnail-data (mf/deref thumbnail-data*) + thumbnail-uri (:uri thumbnail-data) modifiers-ref (mf/with-memo [frame-id] (refs/workspace-modifiers-by-frame-id frame-id)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index 55d191d36a..086a21ae11 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -328,19 +328,18 @@ (mf/with-memo [file-id page-id root-id] (thc/fmt-object-id file-id page-id root-id "component")) - thumbnail-uri* + thumbnail-data* (mf/with-memo [object-id] (refs/workspace-thumbnail-by-id object-id)) + thumbnail-data + (mf/deref thumbnail-data*) + thumbnail-uri - (mf/deref thumbnail-uri*) + (:uri thumbnail-data) - rendered-at* - (mf/with-memo [object-id] - (refs/workspace-thumbnail-rendered-at object-id)) - - rendered-at - (mf/deref rendered-at*) + thumbnail-rendered-at + (:rendered-at thumbnail-data) modified-at (some-> (:modified-at component) (.getTime)) @@ -349,9 +348,9 @@ ;; or the component was modified after the last render stale? (and (some? thumbnail-uri) - (or (nil? rendered-at) + (or (nil? thumbnail-rendered-at) (and (some? modified-at) - (> modified-at rendered-at)))) + (> modified-at thumbnail-rendered-at)))) on-error (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index 0a910d4730..6ffa2d32de 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -258,7 +258,7 @@ dest-shape (cond-> dest-shape (some? thumbnail-data) - (assoc :thumbnail thumbnail-data))] + (assoc :thumbnail (:uri thumbnail-data)))] [:g {:on-pointer-down start-move-position :on-pointer-enter #(reset! is-hover-disabled true) :on-pointer-leave #(reset! is-hover-disabled false)} diff --git a/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs b/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs index fd35d6c5fc..6aaa09e283 100644 --- a/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs +++ b/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs @@ -9,6 +9,9 @@ [app.common.thumbnails :as thc] [app.common.uuid :as uuid] [app.main.data.workspace.thumbnails :as thumbnails] + [app.main.repo :as rp] + [app.util.timers :as tm] + [app.util.webapi :as wapi] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] [frontend-tests.helpers.mock :as mock] @@ -42,257 +45,267 @@ :component-root true}}}}}] (t/is (= #{["frame" root-id] ["component" shape-b-id]} - (#'thumbnails/extract-frame-changes page-id [event [old-data new-data]])))) + (#'thumbnails/extract-frame-changes page-id [event [old-data new-data]]))))) - ;; --- Batch deletion queue state management --- +;; --- Batch deletion queue state management --- - (t/deftest clear-thumbnail-adds-to-deletion-queue - (let [file-id (uuid/next) - object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - uri "blob:http://localhost/test-thumb" - event (thumbnails/clear-thumbnail file-id object-id) - state {:thumbnails {object-id uri}} - result (ptk/update event state)] - ;; Thumbnail removed from the map - (t/is (nil? (get-in result [:thumbnails object-id]))) - ;; Object-id added to the deletion queue with its URI - (t/is (= uri (get-in result [deletion-queue-key object-id]))))) +(t/deftest clear-thumbnail-adds-to-deletion-queue + (let [file-id (uuid/next) + object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri "blob:http://localhost/test-thumb" + event (thumbnails/clear-thumbnail file-id object-id) + state {:thumbnails {object-id {:uri uri :rendered-at nil}}} + result (ptk/update event state)] + ;; Thumbnail removed from the map + (t/is (nil? (get-in result [:thumbnails object-id]))) + ;; Object-id added to the deletion queue with its URI + (t/is (= uri (get-in result [deletion-queue-key object-id]))))) - (t/deftest clear-thumbnail-keeps-other-thumbnails - (let [file-id (uuid/next) - object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - uri1 "blob:http://localhost/thumb-1" - uri2 "blob:http://localhost/thumb-2" - event (thumbnails/clear-thumbnail file-id object-id1) - state {:thumbnails {object-id1 uri1 object-id2 uri2}} - result (ptk/update event state)] - ;; Only the cleared thumbnail is removed - (t/is (nil? (get-in result [:thumbnails object-id1]))) - (t/is (= uri2 (get-in result [:thumbnails object-id2]))) - ;; Only the cleared thumbnail is queued - (t/is (= uri1 (get-in result [deletion-queue-key object-id1]))) - (t/is (nil? (get-in result [deletion-queue-key object-id2]))))) +(t/deftest clear-thumbnail-keeps-other-thumbnails + (let [file-id (uuid/next) + object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri1 "blob:http://localhost/thumb-1" + uri2 "blob:http://localhost/thumb-2" + event (thumbnails/clear-thumbnail file-id object-id1) + state {:thumbnails {object-id1 {:uri uri1 :rendered-at nil} + object-id2 {:uri uri2 :rendered-at nil}}} + result (ptk/update event state)] + ;; Only the cleared thumbnail is removed + (t/is (nil? (get-in result [:thumbnails object-id1]))) + (t/is (= uri2 (get-in result [:thumbnails object-id2 :uri]))) + ;; Only the cleared thumbnail is queued + (t/is (= uri1 (get-in result [deletion-queue-key object-id1]))) + (t/is (nil? (get-in result [deletion-queue-key object-id2]))))) - (t/deftest clear-thumbnail-accumulates-in-queue - (let [file-id (uuid/next) - object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - uri1 "blob:http://localhost/thumb-1" - uri2 "blob:http://localhost/thumb-2" - event1 (thumbnails/clear-thumbnail file-id object-id1) - event2 (thumbnails/clear-thumbnail file-id object-id2) - state {:thumbnails {object-id1 uri1 object-id2 uri2}} - state1 (ptk/update event1 state) - state2 (ptk/update event2 state1)] - ;; Both removed from thumbnails - (t/is (nil? (get-in state2 [:thumbnails object-id1]))) - (t/is (nil? (get-in state2 [:thumbnails object-id2]))) - ;; Both accumulated in the queue - (t/is (= uri1 (get-in state2 [deletion-queue-key object-id1]))) - (t/is (= uri2 (get-in state2 [deletion-queue-key object-id2]))) - (t/is (= 2 (count (get state2 deletion-queue-key)))))) +(t/deftest clear-thumbnail-accumulates-in-queue + (let [file-id (uuid/next) + object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri1 "blob:http://localhost/thumb-1" + uri2 "blob:http://localhost/thumb-2" + event1 (thumbnails/clear-thumbnail file-id object-id1) + event2 (thumbnails/clear-thumbnail file-id object-id2) + state {:thumbnails {object-id1 {:uri uri1 :rendered-at nil} + object-id2 {:uri uri2 :rendered-at nil}}} + state1 (ptk/update event1 state) + state2 (ptk/update event2 state1)] + ;; Both removed from thumbnails + (t/is (nil? (get-in state2 [:thumbnails object-id1]))) + (t/is (nil? (get-in state2 [:thumbnails object-id2]))) + ;; Both accumulated in the queue + (t/is (= uri1 (get-in state2 [deletion-queue-key object-id1]))) + (t/is (= uri2 (get-in state2 [deletion-queue-key object-id2]))) + (t/is (= 2 (count (get state2 deletion-queue-key)))))) - (t/deftest remove-from-deletion-queue-removes-entry - (let [file-id (uuid/next) - object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - event (thumbnails/remove-from-deletion-queue object-id) - state {deletion-queue-key {object-id "blob:http://localhost/thumb"}} - result (ptk/update event state)] - (t/is (nil? (get-in result [deletion-queue-key object-id]))) - (t/is (empty? (get result deletion-queue-key))))) +(t/deftest remove-from-deletion-queue-removes-entry + (let [file-id (uuid/next) + object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + event (thumbnails/remove-from-deletion-queue object-id) + state {deletion-queue-key {object-id "blob:http://localhost/thumb"}} + result (ptk/update event state)] + (t/is (nil? (get-in result [deletion-queue-key object-id]))) + (t/is (empty? (get result deletion-queue-key))))) - (t/deftest remove-from-deletion-queue-keeps-other-entries - (let [file-id (uuid/next) - object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - uri1 "blob:http://localhost/thumb-1" - uri2 "blob:http://localhost/thumb-2" - event (thumbnails/remove-from-deletion-queue object-id1) - state {deletion-queue-key {object-id1 uri1 - object-id2 uri2}} - result (ptk/update event state)] - ;; Only the specified entry is removed - (t/is (nil? (get-in result [deletion-queue-key object-id1]))) - (t/is (= uri2 (get-in result [deletion-queue-key object-id2]))) - (t/is (= 1 (count (get result deletion-queue-key)))))) +(t/deftest remove-from-deletion-queue-keeps-other-entries + (let [file-id (uuid/next) + object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri1 "blob:http://localhost/thumb-1" + uri2 "blob:http://localhost/thumb-2" + event (thumbnails/remove-from-deletion-queue object-id1) + state {deletion-queue-key {object-id1 uri1 + object-id2 uri2}} + result (ptk/update event state)] + ;; Only the specified entry is removed + (t/is (nil? (get-in result [deletion-queue-key object-id1]))) + (t/is (= uri2 (get-in result [deletion-queue-key object-id2]))) + (t/is (= 1 (count (get result deletion-queue-key)))))) - (t/deftest remove-before-clear-cancels-pending-delete +(t/deftest remove-before-clear-cancels-pending-delete + (let [file-id (uuid/next) + object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri "blob:http://localhost/thumb" + ;; Step 1: clear-thumbnail queues the delete + state1 (ptk/update (thumbnails/clear-thumbnail file-id object-id) + {:thumbnails {object-id {:uri uri :rendered-at nil}}}) + ;; Step 2: remove-from-deletion-queue cancels the pending delete + state2 (ptk/update (thumbnails/remove-from-deletion-queue object-id) + state1)] + ;; Thumbnail was removed from :thumbnails map by clear-thumbnail + (t/is (nil? (get-in state2 [:thumbnails object-id]))) + ;; But the deletion queue entry was cancelled by remove-from-deletion-queue + (t/is (nil? (get-in state2 [deletion-queue-key object-id]))) + (t/is (empty? (get state2 deletion-queue-key))))) + +(t/deftest clear-thumbnail-batch-drains-queue + (let [file-id (uuid/next) + object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") + uri1 "blob:http://localhost/thumb-1" + uri2 "blob:http://localhost/thumb-2" + ;; Build up the queue state manually (simulating accumulated clear-thumbnails) + state {deletion-queue-key {object-id1 uri1 object-id2 uri2}} + event (#'thumbnails/clear-thumbnail-batch) + result (ptk/update event state)] + ;; The queue is drained from application state + (t/is (empty? (get result deletion-queue-key))))) + +(t/deftest clear-thumbnail-batch-empty-queue-noop + (let [state {deletion-queue-key {}} + event (#'thumbnails/clear-thumbnail-batch) + result (ptk/update event state)] + ;; Queue key removed from state; rest of state unchanged + (t/is (empty? (get result deletion-queue-key))) + (t/is (= (dissoc state deletion-queue-key) (dissoc result deletion-queue-key))))) + +(t/deftest assoc-thumbnail-adds-to-map + (let [object-id (thc/fmt-object-id (uuid/next) (uuid/next) (uuid/next) "frame") + uri "blob:http://localhost/new-thumb" + event (#'thumbnails/assoc-thumbnail object-id uri) + state {:thumbnails {}} + result (ptk/update event state)] + (t/is (= uri (get-in result [:thumbnails object-id :uri]))) + (t/is (some? (get-in result [:thumbnails object-id :rendered-at]))))) + +(t/deftest duplicate-thumbnail-copies-entry + (let [old-id (thc/fmt-object-id (uuid/next) (uuid/next) (uuid/next) "frame") + new-id (thc/fmt-object-id (uuid/next) (uuid/next) (uuid/next) "frame") + uri "blob:http://localhost/dup-thumb" + entry {:uri uri :rendered-at nil} + event (thumbnails/duplicate-thumbnail old-id new-id) + state {:thumbnails {old-id entry}} + result (ptk/update event state)] + (t/is (= entry (get-in result [:thumbnails old-id]))) + (t/is (= entry (get-in result [:thumbnails new-id]))))) + +;; --- Async WatchEvent tests --- + +(defn- make-obj-ids + "Helper to create n properly-formatted object-ids for a single file." + [file-id n] + (vec (repeatedly n #(thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame")))) + +(t/deftest clear-thumbnail-batch-watch-calls-rpc-with-object-ids + (t/async + done + (let [file-id (uuid/next) + oids (make-obj-ids file-id 3) + state {deletion-queue-key (zipmap oids (repeat "blob:http://test"))} + event (#'thumbnails/clear-thumbnail-batch)] + (ptk/update event state) + (mock/with-mocks + {rp/cmd! mock/rpc-cmd-mock + tm/schedule-on-idle mock/schedule-on-idle-mock} + (fn [done'] + (->> (ptk/watch event state nil) + (rx/reduce conj []) + (rx/subs! + (fn [_] nil) + (fn [err] (t/is (nil? err)) (done')) + (fn [_] + (t/is (= 1 (count @mock/rpc-calls))) + (let [[{:keys [cmd params]}] @mock/rpc-calls] + (t/is (= :delete-file-object-thumbnails cmd)) + (t/is (= (vec oids) (:object-ids params)))) + (done'))))) + done)))) + +(t/deftest clear-thumbnail-batch-watch-partitions-large-batch + (t/async + done + (let [file-id (uuid/next) + oids (make-obj-ids file-id 250) + state {deletion-queue-key (zipmap oids (repeat "blob:http://test"))} + event (#'thumbnails/clear-thumbnail-batch)] + (ptk/update event state) + (mock/with-mocks + {rp/cmd! mock/rpc-cmd-mock + tm/schedule-on-idle mock/schedule-on-idle-mock} + (fn [done'] + (->> (ptk/watch event state nil) + (rx/reduce conj []) + (rx/subs! + (fn [_] nil) + (fn [err] (t/is (nil? err)) (done')) + (fn [_] + (t/is (= 2 (count @mock/rpc-calls))) + (let [[c1 c2] @mock/rpc-calls] + (t/is (= :delete-file-object-thumbnails (:cmd c1))) + (t/is (= :delete-file-object-thumbnails (:cmd c2))) + (t/is (= 200 (count (:object-ids (:params c1))))) + (t/is (= 50 (count (:object-ids (:params c2))))) + (t/is (= (set oids) + (set (concat (:object-ids (:params c1)) + (:object-ids (:params c2))))))) + (done'))))) + done)))) + +(t/deftest clear-thumbnail-batch-watch-revokes-blob-uris + (t/async + done + (let [file-id (uuid/next) + oids (make-obj-ids file-id 2) + uris ["blob:http://localhost/thumb-1" + "blob:http://localhost/thumb-2"] + state {deletion-queue-key (zipmap oids uris)} + event (#'thumbnails/clear-thumbnail-batch)] + (ptk/update event state) + (mock/with-mocks + {rp/cmd! mock/rpc-cmd-mock + wapi/revoke-uri mock/revoke-uri-mock + tm/schedule-on-idle mock/schedule-on-idle-mock} + (fn [done'] + (->> (ptk/watch event state nil) + (rx/reduce conj []) + (rx/subs! + (fn [_] nil) + (fn [err] (t/is (nil? err)) (done')) + (fn [_] + (t/is (= (set uris) (set @mock/revoked-uris))) + (done'))))) + done)))) + +(t/deftest clear-thumbnail-batch-watch-empty-queue-no-rpc + (t/async + done + (let [event (#'thumbnails/clear-thumbnail-batch) + state {}] + (ptk/update event state) + (mock/with-mocks + {rp/cmd! mock/rpc-cmd-mock + tm/schedule-on-idle mock/schedule-on-idle-mock} + (fn [done'] + (->> (ptk/watch event state nil) + (rx/reduce conj []) + (rx/subs! + (fn [_] nil) + (fn [err] (t/is (nil? err)) (done')) + (fn [_] + (t/is (empty? @mock/rpc-calls)) + (done'))))) + done)))) + +(t/deftest clear-thumbnail-watch-emits-batch-after-debounce + (t/async + done (let [file-id (uuid/next) object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") uri "blob:http://localhost/thumb" - ;; Step 1: clear-thumbnail queues the delete - state1 (ptk/update (thumbnails/clear-thumbnail file-id object-id) - {:thumbnails {object-id uri}}) - ;; Step 2: remove-from-deletion-queue cancels the pending delete - state2 (ptk/update (thumbnails/remove-from-deletion-queue object-id) - state1)] - ;; Thumbnail was removed from :thumbnails map by clear-thumbnail - (t/is (nil? (get-in state2 [:thumbnails object-id]))) - ;; But the deletion queue entry was cancelled by remove-from-deletion-queue - (t/is (nil? (get-in state2 [deletion-queue-key object-id]))) - (t/is (empty? (get state2 deletion-queue-key))))) - - (t/deftest clear-thumbnail-batch-drains-queue - (let [file-id (uuid/next) - object-id1 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - object-id2 (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - uri1 "blob:http://localhost/thumb-1" - uri2 "blob:http://localhost/thumb-2" - ;; Build up the queue state manually (simulating accumulated clear-thumbnails) - state {deletion-queue-key {object-id1 uri1 object-id2 uri2}} - event (#'thumbnails/clear-thumbnail-batch) - result (ptk/update event state)] - ;; The queue is drained from application state - (t/is (empty? (get result deletion-queue-key))))) - - (t/deftest clear-thumbnail-batch-empty-queue-noop - (let [state {deletion-queue-key {}} - event (#'thumbnails/clear-thumbnail-batch) - result (ptk/update event state)] - ;; State unchanged when queue is already empty - (t/is (empty? (get result deletion-queue-key))) - (t/is (= state (dissoc result deletion-queue-key))))) - - (t/deftest assoc-thumbnail-adds-to-map - (let [object-id (thc/fmt-object-id (uuid/next) (uuid/next) (uuid/next) "frame") - uri "blob:http://localhost/new-thumb" - event (#'thumbnails/assoc-thumbnail object-id uri) - state {:thumbnails {}} - result (ptk/update event state)] - (t/is (= uri (get-in result [:thumbnails object-id]))))) - - (t/deftest duplicate-thumbnail-copies-entry - (let [old-id (thc/fmt-object-id (uuid/next) (uuid/next) (uuid/next) "frame") - new-id (thc/fmt-object-id (uuid/next) (uuid/next) (uuid/next) "frame") - uri "blob:http://localhost/dup-thumb" - event (thumbnails/duplicate-thumbnail old-id new-id) - state {:thumbnails {old-id uri}} - result (ptk/update event state)] - (t/is (= uri (get-in result [:thumbnails old-id]))) - (t/is (= uri (get-in result [:thumbnails new-id]))))) - - ;; --- Async WatchEvent tests --- - - (defn- make-obj-ids - "Helper to create n properly-formatted object-ids for a single file." - [file-id n] - (vec (repeatedly n #(thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame")))) - - (t/deftest clear-thumbnail-batch-watch-calls-rpc-with-object-ids - (t/async done - (let [file-id (uuid/next) - oids (make-obj-ids file-id 3) - state {deletion-queue-key (zipmap oids (repeat "blob:http://test"))} - event (#'thumbnails/clear-thumbnail-batch)] - (ptk/update event state) - (mock/with-mocks - {app.main.repo/cmd! mock/rpc-cmd-mock - app.util.timers/schedule-on-idle mock/schedule-on-idle-mock} - (fn [done'] - (->> (ptk/watch event state nil) + state {:thumbnails {object-id {:uri uri :rendered-at nil}}} + event (thumbnails/clear-thumbnail file-id object-id)] + (ptk/update event state) + (mock/with-mocks + {rx/timer mock/timer-mock} + (fn [done'] + (let [stream (rx/subject)] + (->> (ptk/watch event state stream) (rx/reduce conj []) (rx/subs! - (fn [_] nil) - (fn [err] (t/is (nil? err)) (done')) - (fn [_] - (t/is (= 1 (count @mock/rpc-calls))) - (let [[{:keys [cmd params]}] @mock/rpc-calls] - (t/is (= :delete-file-object-thumbnails cmd)) - (t/is (= (vec oids) (:object-ids params)))) - (done'))))) - done)))) - - (t/deftest clear-thumbnail-batch-watch-partitions-large-batch - (t/async done - (let [file-id (uuid/next) - oids (make-obj-ids file-id 250) - state {deletion-queue-key (zipmap oids (repeat "blob:http://test"))} - event (#'thumbnails/clear-thumbnail-batch)] - (ptk/update event state) - (mock/with-mocks - {app.main.repo/cmd! mock/rpc-cmd-mock - app.util.timers/schedule-on-idle mock/schedule-on-idle-mock} - (fn [done'] - (->> (ptk/watch event state nil) - (rx/reduce conj []) - (rx/subs! - (fn [_] nil) - (fn [err] (t/is (nil? err)) (done')) - (fn [_] - (t/is (= 2 (count @mock/rpc-calls))) - (let [[c1 c2] @mock/rpc-calls] - (t/is (= :delete-file-object-thumbnails (:cmd c1))) - (t/is (= :delete-file-object-thumbnails (:cmd c2))) - (t/is (= 200 (count (:object-ids (:params c1))))) - (t/is (= 50 (count (:object-ids (:params c2))))) - (t/is (= (set oids) - (set (concat (:object-ids (:params c1)) - (:object-ids (:params c2))))))) - (done'))))) - done)))) - - (t/deftest clear-thumbnail-batch-watch-revokes-blob-uris - (t/async done - (let [file-id (uuid/next) - oids (make-obj-ids file-id 2) - uris ["blob:http://localhost/thumb-1" - "blob:http://localhost/thumb-2"] - state {deletion-queue-key (zipmap oids uris)} - event (#'thumbnails/clear-thumbnail-batch)] - (ptk/update event state) - (mock/with-mocks - {app.main.repo/cmd! (fn [_ _] (rx/of nil)) - app.util.webapi/revoke-uri mock/revoke-uri-mock - app.util.timers/schedule-on-idle mock/schedule-on-idle-mock} - (fn [done'] - (->> (ptk/watch event state nil) - (rx/reduce conj []) - (rx/subs! - (fn [_] nil) - (fn [err] (t/is (nil? err)) (done')) - (fn [_] - (t/is (= (set uris) (set @mock/revoked-uris))) - (done'))))) - done)))) - - (t/deftest clear-thumbnail-batch-watch-empty-queue-no-rpc - (t/async done - (let [event (#'thumbnails/clear-thumbnail-batch) - state {}] - (ptk/update event state) - (mock/with-mocks - {app.main.repo/cmd! mock/rpc-cmd-mock - app.util.timers/schedule-on-idle mock/schedule-on-idle-mock} - (fn [done'] - (->> (ptk/watch event state nil) - (rx/reduce conj []) - (rx/subs! - (fn [_] nil) - (fn [err] (t/is (nil? err)) (done')) - (fn [_] - (t/is (empty? @mock/rpc-calls)) - (done'))))) - done)))) - - (t/deftest clear-thumbnail-watch-emits-batch-after-debounce - (t/async done - (let [file-id (uuid/next) - object-id (thc/fmt-object-id file-id (uuid/next) (uuid/next) "frame") - uri "blob:http://localhost/thumb" - state {:thumbnails {object-id uri}} - event (thumbnails/clear-thumbnail file-id object-id)] - (ptk/update event state) - (mock/with-mocks - {beicon.v2.core/timer mock/timer-mock} - (fn [done'] - (->> (ptk/watch event state nil) - (rx/reduce conj []) - (rx/subs! - (fn [_] nil) - (fn [err] (t/is (nil? err)) (done')) (fn [events] (t/is (= 1 (count events))) (t/is (ptk/event? (first events))) - (done'))))) - done))))) + (done')) + (fn [err] (t/is (nil? err)) (done')) + nil)))) + done)))) diff --git a/frontend/test/frontend_tests/helpers/mock.cljc b/frontend/test/frontend_tests/helpers/mock.cljc index fce14876a3..fad0e8c3d1 100644 --- a/frontend/test/frontend_tests/helpers/mock.cljc +++ b/frontend/test/frontend_tests/helpers/mock.cljc @@ -106,9 +106,12 @@ (defn rpc-cmd-mock "Records [cmd params] in [[rpc-calls]], returns `(rx/of nil)`." - [cmd params] - (swap! rpc-calls conj {:cmd cmd :params params}) - (rx/of nil)) + ([cmd params] + (swap! rpc-calls conj {:cmd cmd :params params}) + (rx/of nil)) + ([cmd params _opts] + (swap! rpc-calls conj {:cmd cmd :params params}) + (rx/of nil))) (defn revoke-uri-mock "Records `uri` in [[revoked-uris]]." @@ -117,8 +120,10 @@ (defn schedule-on-idle-mock "Calls `f` immediately instead of deferring to the idle queue." - [f] - (f)) + ([_ms f] + (f)) + ([f] + (f))) (defn timer-mock "Returns `(rx/of :immediate)` so debounce timers fire instantly