penpot/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs
2026-06-10 14:51:46 +02:00

111 lines
4.8 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 INC Sucursal en España SL
(ns frontend-tests.render-wasm.process-objects-test
"Unit tests for wasm.api/process-objects.
The key invariant: process-objects must call set-object for every shape
and forward ALL shapes to a single process-pending invocation.
Without this batching the async font-load callback only covers the shape
that triggered the font fetch. Subsequent text shapes that share the same
font URL get no callback (fetch-font returns nil when the URL is already
in :fetching) and are permanently stuck with fallback-font layout metrics."
(:require
[app.render-wasm.api :as wasm.api]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]))
;; ---------------------------------------------------------------------------
;; Helpers
;; ---------------------------------------------------------------------------
(defn- make-shape [type]
{:id (random-uuid) :type type})
(defn- with-mocks*
"Temporarily replaces wasm.api/set-object and wasm.api/process-pending,
calls (thunk), then restores the originals."
[mock-set-object mock-process-pending thunk]
(let [orig-set-object wasm.api/set-object
orig-process-pending wasm.api/process-pending]
(set! wasm.api/set-object mock-set-object)
(set! wasm.api/process-pending mock-process-pending)
(try
(thunk)
(finally
(set! wasm.api/set-object orig-set-object)
(set! wasm.api/process-pending orig-process-pending)))))
;; ---------------------------------------------------------------------------
;; Tests
;; ---------------------------------------------------------------------------
(t/deftest process-objects-calls-set-object-for-every-shape
"Each shape in the input must go through set-object exactly once."
(let [shapes [(make-shape :text) (make-shape :text) (make-shape :rect)]
visited-ids (atom [])
mock-set (fn [s] (swap! visited-ids conj (:id s)) {:thumbnails [] :full []})
mock-pend (fn [_sh _t _f _cb] nil)]
(with-mocks* mock-set mock-pend #(wasm.api/process-objects shapes))
(t/is (= (mapv :id shapes) @visited-ids)
"set-object called once per shape in order")))
(t/deftest process-objects-calls-process-pending-once-with-all-shapes
"process-pending must receive ALL shapes in a single call.
This is the invariant that makes the async font-load callback update text
layouts for every text shape, not just the first one that triggered the
font fetch."
(let [shapes [(make-shape :text) (make-shape :text)]
captured (atom nil)
mock-set (fn [_s] {:thumbnails [] :full []})
mock-pend (fn [sh t f cb] (reset! captured {:shapes sh :thumbnails t :full f :cb cb}))]
(with-mocks* mock-set mock-pend #(wasm.api/process-objects shapes))
(t/is (some? @captured)
"process-pending was called")
(t/is (= 2 (count (:shapes @captured)))
"process-pending received all shapes")
(t/is (= (set (map :id shapes))
(set (map :id (:shapes @captured))))
"process-pending received the correct shape objects")))
(t/deftest process-objects-accumulates-callbacks-across-shapes
"Pending font/image callbacks from all shapes must be merged into the single
process-pending call. This covers the deduplication scenario: when a second
text shape shares the same font (fetch-font returns nil because the URL is
already in :fetching), the callback from the first shape still covers the
second shape because both are in the :shapes list passed to process-pending."
(let [shape-a (make-shape :text)
shape-b (make-shape :text)
shapes [shape-a shape-b]
font-cb (fn [] (rx/of true))
captured (atom nil)
;; Simulate deduplication: shape-a triggers a real font fetch,
;; shape-b gets an empty pending list (same URL already in :fetching).
mock-set
(fn [s]
(if (= (:id s) (:id shape-a))
{:thumbnails [{:key "font-url" :callback font-cb}] :full []}
{:thumbnails [] :full []}))
mock-pend
(fn [sh t f _cb] (reset! captured {:shapes sh :thumbnails t :full f}))]
(with-mocks* mock-set mock-pend #(wasm.api/process-objects shapes))
;; The single font callback from shape-a is present...
(t/is (= 1 (count (:thumbnails @captured)))
"The font callback from shape-a is forwarded")
;; ...and BOTH shapes are in the list, so when the font loads and
;; process-pending fires update-text-layouts, it covers shape-b too.
(t/is (= 2 (count (:shapes @captured)))
"Both shapes are in process-pending so font-load covers all of them")))