From 23746893e66f66520dcf6aa8274c3df9b3d4b504 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Fri, 2 Oct 2026 13:11:57 +0200 Subject: [PATCH] :construction: WIP Step 20 test last round of features --- .../benches/contracts_test.cljs | 7 ++ .../benches/geometry_scenes_test.cljs | 81 +++++++++++++++++++ .../benches/lifecycle_test.cljs | 49 ++++++++--- .../frontend_tests/benches/protocol_test.cljs | 61 ++++++++++++++ 4 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 frontend/test/frontend_tests/benches/geometry_scenes_test.cljs diff --git a/frontend/test/frontend_tests/benches/contracts_test.cljs b/frontend/test/frontend_tests/benches/contracts_test.cljs index e87683620d..1c1d8e750a 100644 --- a/frontend/test/frontend_tests/benches/contracts_test.cljs +++ b/frontend/test/frontend_tests/benches/contracts_test.cljs @@ -83,6 +83,13 @@ (let [collected (rect-cases)] (t/is (= [:rects/load :rects/pan :rects/zoom] (mapv :id collected))))) +(t/deftest real-rectangle-bodies-stay-out-of-node-descriptors + (let [ids [:rects/load :rects/pan :rects/zoom]] + (t/is (every? #(fn? (:run! (core/registered-case %))) ids)) + (t/is (= ids (mapv :id (rect-cases))) + "collection succeeds with the executable bodies present") + (t/is (every? #(not (contains? % :run!)) (rect-cases))))) + (t/deftest collected-cases-survive-the-wire (doseq [case-desc (rect-cases)] (t/is (not (contains? case-desc :run!)) (str (:id case-desc))) diff --git a/frontend/test/frontend_tests/benches/geometry_scenes_test.cljs b/frontend/test/frontend_tests/benches/geometry_scenes_test.cljs new file mode 100644 index 0000000000..fe5a3c95ab --- /dev/null +++ b/frontend/test/frontend_tests/benches/geometry_scenes_test.cljs @@ -0,0 +1,81 @@ +;; 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.geometry-scenes-test + (:require + [app.common.types.path :as path] + [app.common.uuid :as uuid] + [benches.render-wasm.cases :as cases] + [benches.render-wasm.scenes.common :as common] + [benches.render-wasm.scenes.masks :as masks] + [benches.render-wasm.scenes.paths :as paths] + [benches.render-wasm.scenes.plus :as plus] + [cljs.test :as t :include-macros true] + [frontend-tests.benches.test-helpers :as helpers])) + +(t/deftest curved-paths-use-canonical-content-and-stable-order + (let [params {:count 12 :width 1920 :height 1080 :seed 42} + scene (paths/build params) + again (paths/build params) + shapes (helpers/children-of scene uuid/zero) + first (first shapes) + content (vec (:content first))] + (t/is (= 12 (count shapes))) + (t/is (= scene again) "same scene parameters and seed reproduce content and ids") + (t/is (path/content? (:content first))) + (t/is (= [:move-to :curve-to :line-to :close-path] + (mapv :command content))) + (t/is (pos? (:width (:selrect first)))) + (t/is (pos? (:height (:selrect first)))) + (t/is (= (mapv :id shapes) (mapv :id (rest (common/upload-order scene))))) + (t/is (= scene (common/validate! scene))))) + +(t/deftest plus-paths-have-two-crossing-open-lines + (let [scene (plus/build {:count 8 :width 1920 :height 1080 :seed 42}) + first (first (helpers/children-of scene uuid/zero)) + segments (vec (:content first)) + [a b c d] segments] + (t/is (= 8 (count (helpers/children-of scene uuid/zero)))) + (t/is (= [:move-to :line-to :move-to :line-to] + (mapv :command segments))) + (t/is (= (get-in a [:params :y]) (get-in b [:params :y])) + "the first line is horizontal") + (t/is (< (get-in a [:params :x]) (get-in b [:params :x]))) + (t/is (= (get-in c [:params :x]) (get-in d [:params :x])) + "the second line is vertical") + (t/is (< (get-in c [:params :y]) (get-in d [:params :y]))) + (t/is (seq (:strokes first))) + (t/is (= scene (common/validate! scene))))) + +(t/deftest mask-child-order-and-descendants-are-reachable + (let [scene (masks/build {:width 1920 :height 1080 :seed 42}) + groups (helpers/children-of scene uuid/zero) + group (first groups) + children (helpers/children-of scene (:id group)) + mask (first children) + uploaded (common/upload-order scene)] + (t/is (= 24 (count groups))) + (t/is (= 241 (count (:objects scene))) "root, groups and their nine children") + (t/is (= (:id group) (get-in scene [:refs [:mask 0]]))) + (t/is (true? (:masked-group group))) + (t/is (= :circle (:type mask)) "the first child is the mask") + (t/is (= 9 (count children))) + (t/is (every? #(= (:id group) (:parent-id %)) children)) + (t/is (= (:selrect mask) (:selrect group))) + (t/is (= uuid/zero (:id (first uploaded)))) + (t/is (= (:id group) (:id (second uploaded)))) + (t/is (= (:id mask) (:id (nth uploaded 2)))) + (t/is (= (count (:objects scene)) (count uploaded))) + (t/is (= scene (common/validate! scene))))) + +(t/deftest camera-cases-share-each-scene-seed + (doseq [scene-id [:paths :plus :masks]] + (let [selected (->> (cases/collect-cases {:master-seed 42}) + (filter #(= scene-id (:scene %))) + vec)] + (t/is (= #{"load" "pan" "zoom"} (set (map (comp name :id) selected)))) + (t/is (= 1 (count (set (map :scene-seed selected))))) + (t/is (every? #(not (contains? % :run!)) selected))))) diff --git a/frontend/test/frontend_tests/benches/lifecycle_test.cljs b/frontend/test/frontend_tests/benches/lifecycle_test.cljs index 00cfa9c223..b8d162e225 100644 --- a/frontend/test/frontend_tests/benches/lifecycle_test.cljs +++ b/frontend/test/frontend_tests/benches/lifecycle_test.cljs @@ -35,11 +35,13 @@ "Transit request for `browser/load-scene`. `overrides` change the collected case descriptor." [seed overrides] - (transit/encode-str - {:seed seed - :case (merge (load-case) overrides) - :module-url (factory-url) - :wasm-url "./fake.wasm"})) + (let [base (or (some #(when (= (:id %) (:id overrides)) %) + (cases/collect-cases {:master-seed 42})) + (load-case))] + (transit/encode-str + {:case (merge base {:scene-seed seed} overrides) + :module-url (factory-url) + :wasm-url "./fake.wasm"}))) (defn- expect-failed-args "Calls `load-scene` with `args` (never touching DOM/FFI: validation runs @@ -62,11 +64,11 @@ (t/deftest missing-seed-fails-before-timers (t/async done - (expect-failed-args (transit/encode-str {:case (load-case)}) true done))) + (expect-failed-args (transit/encode-str {:case (dissoc (load-case) :scene-seed)}) true done))) (t/deftest missing-case-fails-before-timers (t/async done - (expect-failed-args (transit/encode-str {:seed 42}) true done))) + (expect-failed-args (transit/encode-str {}) true done))) (t/deftest malformed-transit-fails-before-timers (t/async done @@ -87,6 +89,22 @@ true done))) +(t/deftest missing-compiled-body-fails-before-claim + (t/async done + (expect-failed-args (scene-args 42 {:id :rects/missing}) true done))) + +(t/deftest wrong-scene-body-fails-before-claim + (let [id :rects/wrong-scene + descriptor (load-case)] + (core/register-case! {:id id :scene :other :ns "lifecycle-test" + :run! (fn [_] nil)}) + (try + (let [request (transit/encode-str {:case (assoc descriptor :id id)}) + parsed (@#'browser/parse-load-request request)] + (t/is (= "invalid-args" (get-in parsed [:error :phase])))) + (finally + (core/unregister-case! id))))) + (t/deftest non-finite-view-values-fail-before-timers (t/async done (expect-failed-args (scene-args 42 {:view {:scale js/Infinity :x 0 :y 0}}) @@ -418,6 +436,20 @@ (cleanup) (done))))))) +(t/deftest rejected-camera-body-releases-owner + (t/async done + (with-entry-env + (fn [cleanup] + (unchecked-set js/globalThis "__benchThrowIn" "_render_from_cache") + (load-result (scene-args 7 {:id :rects/pan :context :reuse}) + (fn [m] + (t/is (= "failed" (:status m))) + (t/is (= "interact" (:phase m))) + (t/is (= 1 (effect-count (read-calls) "remove"))) + (t/is (nil? @@#'browser/canvas*)) + (cleanup) + (done))))))) + (t/deftest rejected-import-resolves-module-init-failure (t/async done ;; Needs the entry env for the import polyfill, even though the import @@ -425,8 +457,7 @@ (with-entry-env (fn [cleanup] (load-result (transit/encode-str - {:seed 7 - :case (load-case) + {:case (assoc (load-case) :scene-seed 7) :module-url "data:text/javascript,this is not valid javascript((("}) (fn [m] (t/is (= "failed" (:status m))) diff --git a/frontend/test/frontend_tests/benches/protocol_test.cljs b/frontend/test/frontend_tests/benches/protocol_test.cljs index 7d0fc424c8..00cbc72e3f 100644 --- a/frontend/test/frontend_tests/benches/protocol_test.cljs +++ b/frontend/test/frontend_tests/benches/protocol_test.cljs @@ -220,3 +220,64 @@ (.catch (fn [cause] (t/is false (str "must resolve, threw: " cause)) (done))))))) + +(t/deftest camera-session-chains-promises-and-records-authored-sleep + (t/async done + (let [clock (fake-clock 0) + views (atom []) + events (atom []) + hooks (assoc (drain-hooks {:clock clock :script [2] :calls (atom [])}) + :frame (fn [] + (swap! events conj :frame) + ((:advance! clock) 16) + (js/Promise.resolve ((:now clock)))) + :set-view (fn [view] + (swap! views conj view) + ((:advance! clock) 2)) + :render-from-cache (fn [] + (swap! events conj :cached) + ((:advance! clock) 3)) + :set-view-end (fn [] + (swap! events conj :end) + ((:advance! clock) 5))) + base {:scale 1 :x 0 :y 0} + target {:scale 2 :x 100 :y 40}] + (-> (protocol/start-camera! {:hooks hooks :view base} {:settle-ms 100}) + (protocol/animate-view! {:to target :steps 3}) + (protocol/sleep! 25) + (protocol/finish-camera!) + (.then (fn [m] + (t/is (apply < (map :x @views)) + "intermediate camera views advance toward the target") + (t/is (= target (last @views))) + (t/is (= [:frame :cached :frame :cached :frame :cached :end] @events)) + (t/is (= 3 (count (:cached-slices m)))) + (t/is (= [5 5 5] (mapv :view-and-preview-ms (:cached-slices m))) + "call span also includes the view update") + (t/is (= [3 3 3] (mapv :duration-ms (:cached-slices m)))) + (t/is (= [{:requested-ms 25 :actual-ms 25}] (:authored-sleeps m))) + (t/is (= 88 (:active-ms m)) + "three rAF waits, view/cache calls and authored pause") + (t/is (= 100 (:settling-actual-ms m))) + (t/is (= 193 (:interact-ms m)) + "active, settle and camera end are one span") + (done))) + (.catch (fn [cause] + (t/is false (str "must resolve, threw: " cause)) + (done))))))) + +(t/deftest camera-session-rejects-hook-errors + (t/async done + (let [clock (fake-clock 0) + hooks (assoc (drain-hooks {:clock clock :script [2] :calls (atom [])}) + :render-from-cache (fn [] (throw (ex-info "cache failed" {:phase "preview"}))))] + (-> (protocol/start-camera! {:hooks hooks :view {:scale 1 :x 0 :y 0}} + {:settle-ms 100}) + (protocol/preview-view! {:scale 1 :x 10 :y 0}) + (protocol/finish-camera!) + (.then (fn [_] + (t/is false "preview error must reject") + (done))) + (.catch (fn [cause] + (t/is (= "cache failed" (ex-message cause))) + (done)))))))