2026-10-28 19:17:38 +01:00

223 lines
10 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.benches.protocol-test
"Tests drain, restore and interact with an injected clock and renderer.
Browser entry tests cover load and warm pan with a fake module."
(:require
[app.common.render-wasm.wasm :as wasm]
[benches.render-wasm.protocol :as protocol]
[cljs.test :as t :include-macros true]))
(defn- fake-clock
"Controllable ms clock. `advance!` moves it; tests drive time by hand."
[start]
(let [t (atom start)]
{:now (fn [] @t)
:advance! (fn [ms] (swap! t + ms) @t)}))
(defn- drain-hooks
"Fake hooks driving `:render` through `script` (a vector of frame types
returned in order), recording observed flags in `calls`. `:frame`
advances 16 ms per rAF; `:sleep` advances the clock without yielding."
[{:keys [clock script calls check]}]
(let [remaining (atom (vec script))]
{:now (:now clock)
:frame (fn []
((:advance! clock) 16)
(js/Promise.resolve ((:now clock))))
:sleep (fn [ms]
((:advance! clock) ms)
(js/Promise.resolve nil))
:check (or check (fn [] nil))
:render (fn [_timestamp flags]
(swap! calls conj flags)
(let [next (first @remaining)]
(swap! remaining rest)
next))
:render-from-cache (fn [] 0)
:set-view (fn [_view] nil)
:set-view-start (fn [] nil)
:set-view-end (fn [] nil)}))
(t/deftest immediate-full-reports-equal-boundaries
(t/async done
(let [clock (fake-clock 1000)
calls (atom [])
hooks (drain-hooks {:clock clock :script [2] :calls calls})]
(-> (protocol/drain hooks {:flags 0 :origin 1000 :immediate true})
(.then (fn [{:keys [slices viewport-ready-ms full-ms]}]
(t/is (= 1 (count slices)))
(t/is (= {:timestamp 1000
:flags 0
:frame-type 2
:duration-ms 0}
(first slices)))
(t/is (= full-ms viewport-ready-ms)
"immediate Full supplies both boundaries")
(done)))
(.catch (fn [cause]
(t/is false (str "must resolve, threw: " cause))
(done)))))))
(t/deftest progressive-frames-propagate-flags
(t/async done
(let [clock (fake-clock 0)
calls (atom [])
hooks (drain-hooks {:clock clock :script [1 3 2] :calls calls})]
(-> (protocol/drain hooks {:flags 0 :origin 0 :immediate false})
(.then (fn [{:keys [slices viewport-ready-ms full-ms]}]
(t/is (= [0 1 3] @calls)
"the previous frame type becomes the next flags")
(t/is (= [1 3 2] (mapv :frame-type slices)))
(t/is (< viewport-ready-ms full-ms)
"ViewportReady lands before Full")
(t/is (= (:timestamp (nth slices 1)) (+ 16 16))
"later frames wait for rAF")
(done)))
(.catch (fn [cause]
(t/is false (str "must resolve, threw: " cause))
(done)))))))
(t/deftest partial-jumps-straight-to-full
(t/async done
(let [clock (fake-clock 0)
calls (atom [])
hooks (drain-hooks {:clock clock :script [1 2] :calls calls})]
(-> (protocol/drain hooks {:flags 0 :origin 0 :immediate true})
(.then (fn [{:keys [slices viewport-ready-ms full-ms]}]
(t/is (= [0 1] @calls))
(t/is (= [1 2] (mapv :frame-type slices)))
(t/is (= viewport-ready-ms full-ms)
"Full as the first completion supplies both boundaries")
(done)))
(.catch (fn [cause]
(t/is false (str "must resolve, threw: " cause))
(done)))))))
(t/deftest unknown-frame-types-reject
(t/async done
;; One async context chaining the bad values: a `t/async` inside
;; `doseq` would never run (the doseq returns nil to the runner).
(letfn [(step [remaining]
(if (empty? remaining)
(done)
(let [bad (first remaining)
clock (fake-clock 0)
calls (atom [])
hooks (drain-hooks {:clock clock :script [bad] :calls calls})]
(-> (protocol/drain hooks {:flags 0 :origin 0 :immediate true})
(.then (fn [_]
(t/is false (str "must reject for frame type " bad))
(done)))
(.catch (fn [cause]
(t/is (= :benches.render-wasm.protocol/unexpected-frame-type
(:type (ex-data cause)))
(str "frame type " bad))
(step (rest remaining))))))))]
(step [0 7 js/NaN]))))
(t/deftest cancellation-stops-further-renders
(t/async done
(let [clock (fake-clock 0)
calls (atom [])
fired (atom 0)
hooks (drain-hooks {:clock clock
:script [1 1 1 1]
:calls calls
:check (fn []
(swap! fired inc)
(when (> @fired 2)
(throw (ex-info "canceled" {:phase "test"}))))})]
(-> (protocol/drain hooks {:flags 0 :origin 0 :immediate true})
(.then (fn [_]
(t/is false "must reject once check throws")
(done)))
(.catch (fn [cause]
(t/is (= "canceled" (ex-message cause)))
(t/is (= 1 (count @calls)) "cancellation stops before the next render")
(t/is (= 3 @fired) "guard fires once before and once after the render")
(done)))))))
(t/deftest restore-drains-with-sync-tiles-immediately
(t/async done
(let [clock (fake-clock 500)
calls (atom [])
order (atom [])
hooks (assoc (drain-hooks {:clock clock :script [1 3 2] :calls calls})
:set-view-start (fn [] (swap! order conj :start) nil)
:set-view (fn [view]
(swap! order conj [:view view])
nil)
:set-view-end (fn [] (swap! order conj :end) nil))
view {:scale 1 :x 10 :y 20}]
(-> (protocol/restore hooks view)
(.then (fn [{:keys [slices]}]
(t/is (= [:start [:view view] :end] @order)
"camera resets before the drain")
(t/is (= [4 1 3] @calls)
"restore drains with SyncTiles first")
(t/is (= [1 3 2] (mapv :frame-type slices)))
(done)))
(.catch (fn [cause]
(t/is false (str "must resolve, threw: " cause))
(done)))))))
(t/deftest interact-times-gesture-settle-and-final-drain
(t/async done
(let [clock (fake-clock 0)
renders (atom [])
cached (atom [])
views (atom [])
hooks {:now (:now clock)
:frame (fn []
((:advance! clock) 16)
(js/Promise.resolve ((:now clock))))
:sleep (fn [ms]
((:advance! clock) ms)
(js/Promise.resolve nil))
:check (fn [] nil)
:render (fn [_timestamp flags]
(swap! renders conj flags)
2)
:render-from-cache (fn []
(swap! cached conj true)
((:advance! clock) 3)
nil)
:set-view (fn [view]
(swap! views conj view)
nil)
:set-view-start (fn [] nil)
:set-view-end (fn []
((:advance! clock) 5)
nil)}
frames [{:scale 1 :x 0 :y 0} {:scale 1 :x 100 :y 0}]]
(-> (protocol/interact hooks {:frames frames :settle-ms 100})
(.then (fn [m]
(t/is (= frames @views) "every gesture frame sets the view")
(t/is (= 2 (count @cached)) "one cached preview per frame")
(t/is (= 2 (count (:cached-slices m))))
(t/is (every? #(contains? % :timestamp) (:cached-slices m)))
(t/is (every? #(contains? % :duration-ms) (:cached-slices m))
"cached slices keep their own shape")
(t/is (= 100 (:settling-requested-ms m)))
(t/is (= 100 (:settling-actual-ms m))
"fake sleep advances the clock exactly")
(t/is (= 5 (:set-view-end-ms m)))
(t/is (= [4] @renders) "final drain starts with SyncTiles")
(t/is (>= (:time-to-full-ms m) (:set-view-end-ms m))
"finalization includes _set_view_end")
(t/is (= (:time-to-viewport-ready-ms m) (:time-to-full-ms m))
"immediate final drain gives equal boundaries")
(t/is (= 108 (:last-input-to-full-ms m))
"last input at 35, Full at 143: settle + set_view_end + drain")
(t/is (= [wasm/FRAME_TYPE_FULL] (mapv :frame-type (:slices m)))
"frame is full")
(done)))
(.catch (fn [cause]
(t/is false (str "must resolve, threw: " cause))
(done)))))))