From f967a0fc8317bc30a6e6bca12a29d0f20038b7b4 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 23 Jun 2026 11:21:53 +0200 Subject: [PATCH] :sparkles: Add improvements for frontend tests (#10380) --- common/src/app/common/logging.cljc | 5 +- frontend/package.json | 4 +- frontend/src/app/main/errors.cljs | 4 +- frontend/src/app/main/features.cljs | 2 - .../data/workspace_thumbnails_test.cljs | 20 +- .../test/frontend_tests/helpers/mock.cljc | 136 ++++ .../test/frontend_tests/helpers/mock.cljs | 103 --- .../test/frontend_tests/helpers/state.cljs | 11 +- .../logic/components_and_tokens.cljs | 2 +- .../logic/copying_and_duplicating_test.cljs | 724 +++++++++--------- .../test/frontend_tests/main_errors_test.cljs | 4 +- .../frontend_tests/plugins/page_test.cljs | 47 +- frontend/test/frontend_tests/runner.cljs | 116 +-- .../tokens/import_export_test.cljs | 7 +- 14 files changed, 632 insertions(+), 553 deletions(-) create mode 100644 frontend/test/frontend_tests/helpers/mock.cljc delete mode 100644 frontend/test/frontend_tests/helpers/mock.cljs diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index c8f28bd029..0c96e7c22e 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -332,8 +332,9 @@ (defn setup! [{:as config}] (run! (fn [[logger level]] - (let [logger (if (keyword? logger) (name logger) logger)] - (l/set-level! logger level))) + (let [logger (if (keyword? logger) (name logger) logger) + level (level->int level)] + (.set ^js/Map loggers logger level))) config))) (defmacro raw! diff --git a/frontend/package.json b/frontend/package.json index c35da555a5..74f85808cf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,8 +32,8 @@ "lint:clj": "clj-kondo --parallel --lint ../common/src src/", "lint:js": "exit 0", "lint:scss": "pnpm exec stylelint '{src,resources}/**/*.scss'", - "build:test": "clojure -M:dev:shadow-cljs compile test", - "test": "pnpm run build:wasm && pnpm run build:test && node target/tests/test.js", + "build:test": "pnpm run build:wasm && clojure -M:dev:shadow-cljs compile test", + "test": "[ -f target/tests/test.js ] || pnpm run build:test; node target/tests/test.js", "test:quiet": "node ./scripts/test-quiet.js", "test:storybook": "vitest run --project=storybook", "watch:test": "mkdir -p target/tests && concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests --exec 'node target/tests/test.js'\"", diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 31a46b13b0..5b7bb40f5b 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -104,7 +104,9 @@ instead of recursing until the call-stack is exhausted." [error] (if @handling-error? - (.error js/console "[on-error] re-entrant call suppressed" error) + (do + (js/console.error "[on-error] re-entrant call suppressed") + (ex/print-throwable error)) (do (vreset! handling-error? true) (try diff --git a/frontend/src/app/main/features.cljs b/frontend/src/app/main/features.cljs index 1cab3b49df..58b33ce313 100644 --- a/frontend/src/app/main/features.cljs +++ b/frontend/src/app/main/features.cljs @@ -21,8 +21,6 @@ [potok.v2.core :as ptk] [rumext.v2 :as mf])) -(log/set-level! :trace) - (def global-enabled-features (cfeat/get-enabled-features cf/flags)) diff --git a/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs b/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs index 2ccf1c51fc..fd35d6c5fc 100644 --- a/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs +++ b/frontend/test/frontend_tests/data/workspace_thumbnails_test.cljs @@ -187,8 +187,8 @@ 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} + {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 []) @@ -211,8 +211,8 @@ 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} + {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 []) @@ -242,9 +242,9 @@ 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} + {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 []) @@ -262,8 +262,8 @@ 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} + {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 []) @@ -284,7 +284,7 @@ event (thumbnails/clear-thumbnail file-id object-id)] (ptk/update event state) (mock/with-mocks - {#'beicon.v2.core/timer mock/timer-mock} + {beicon.v2.core/timer mock/timer-mock} (fn [done'] (->> (ptk/watch event state nil) (rx/reduce conj []) diff --git a/frontend/test/frontend_tests/helpers/mock.cljc b/frontend/test/frontend_tests/helpers/mock.cljc new file mode 100644 index 0000000000..fce14876a3 --- /dev/null +++ b/frontend/test/frontend_tests/helpers/mock.cljc @@ -0,0 +1,136 @@ +;; 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 + +(ns frontend-tests.helpers.mock + "Async-first mocking primitives for ClojureScript tests. + + Uses `set!` to install and restore mocks so that they remain active + across asynchronous boundaries (Promises, RxJS subscriptions, callbacks). + `with-redefs` cannot be used here because it restores bindings when the + body exits, which is too early for async code. + + Recording atoms (`rpc-calls`, `revoked-uris`) persist across the entire + test lifecycle, making captured data inspectable regardless of whether + callbacks fire synchronously or asynchronously. + + The `with-mocks` helper wraps the lifecycle: + 1. Reset recording atoms + 2. Save original var values, install mocks via `set!` + 3. Execute `(test-fn inner-done)` + 4. `inner-done` restores originals and calls `outer-done` + (typically `cljs.test/async`'s done). + + Usage: `(with-mocks {ns/sym mock-fn, ...} test-fn done)`" + #?(:cljs (:require + [beicon.v2.core :as rx])) + #?(:cljs (:require-macros [frontend-tests.helpers.mock]))) + +;; ═══════════════════════════════════════════════════════════════ +;; Macro (compile-time, Clojure only) +;; ═══════════════════════════════════════════════════════════════ + +#?(:clj + (defmacro with-mocks + "Resets recording atoms, installs `mocks` via `set!`, then + calls `(test-fn inner-done)`. Original var values are restored + when `inner-done` is called. + + `mocks` is a map of sym → mock-fn + (e.g. `{app.main.repo/cmd! mock-fn}`). + + `inner-done` restores the originals and calls `outer-done` (the + `cljs.test/async` `done` callback). + + Example: + + (t/deftest my-async-test + (t/async done + (mock/with-mocks + {app.main.repo/cmd! mock/rpc-cmd-mock} + (fn [done'] + (->> (some-async-flow) + (rx/subs! + (fn [v] ...) + (fn [err] (done')) + (fn [] (done'))))))))" + [mocks test-fn outer-done] + (let [entries (map identity mocks) + gen-pairs (mapv (fn [[qsym _mock]] + {:qsym qsym + :osym (gensym "orig-")}) + entries) + ;; [orig-123 app.main.repo/cmd!, orig-456 app.util.timers/schedule-on-idle, ...] + let-bindings (vec (mapcat + (fn [{:keys [qsym osym]}] + [osym qsym]) + gen-pairs)) + install-exprs (mapv + (fn [[_qsym mock-fn] {:keys [qsym]}] + `(set! ~qsym ~mock-fn)) + entries + gen-pairs) + restore-exprs (mapv + (fn [{:keys [qsym osym]}] + `(set! ~qsym ~osym)) + gen-pairs)] + `(do + (frontend-tests.helpers.mock/reset-state!) + (let ~let-bindings + ~@install-exprs + (~test-fn (fn inner-done# [] + ~@restore-exprs + (~outer-done)))))))) + +;; ═══════════════════════════════════════════════════════════════ +;; Runtime (ClojureScript only) +;; ═══════════════════════════════════════════════════════════════ + +#?(:cljs + (do + ;; Recording atoms + ;; ═══════════════════════════════════════════════════════════════ + + (def rpc-calls + "Atom accumulating mocked `rp/cmd!` calls as `{:cmd kw :params map}`." + (atom [])) + + (def revoked-uris + "Atom accumulating URIs passed to `wapi/revoke-uri`." + (atom [])) + + ;; Mock implementations + ;; ═══════════════════════════════════════════════════════════════ + + (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)) + + (defn revoke-uri-mock + "Records `uri` in [[revoked-uris]]." + [uri] + (swap! revoked-uris conj uri)) + + (defn schedule-on-idle-mock + "Calls `f` immediately instead of deferring to the idle queue." + [f] + (f)) + + (defn timer-mock + "Returns `(rx/of :immediate)` so debounce timers fire instantly + during tests." + [_ms] + (rx/of :immediate)) + + ;; Lifecycle + ;; ═══════════════════════════════════════════════════════════════ + + (defn reset-state! + "Clear all recording atoms. Called automatically by [[with-mocks]]." + [] + (reset! rpc-calls []) + (reset! revoked-uris [])))) diff --git a/frontend/test/frontend_tests/helpers/mock.cljs b/frontend/test/frontend_tests/helpers/mock.cljs deleted file mode 100644 index bce234f4cf..0000000000 --- a/frontend/test/frontend_tests/helpers/mock.cljs +++ /dev/null @@ -1,103 +0,0 @@ -;; 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 - -(ns frontend-tests.helpers.mock - "Async-first mocking primitives for ClojureScript tests. - - Uses `with-redefs` — the standard CLJS mechanism for rebinding Vars - within a dynamic scope. Recording atoms (`rpc-calls`, `revoked-uris`) - persist across the entire test lifecycle, making captured data - inspectable regardless of whether callbacks fire synchronously or - asynchronously. - - The `with-mocks` helper wraps the lifecycle: - 1. Reset recording atoms - 2. Install mocks via `with-redefs` - 3. Execute `(test-fn inner-done)` - 4. `inner-done` calls `outer-done` (typically `cljs.test/async`'s done) - - Since all mock functions return synchronous `rx/of` observables, - callbacks always fire within the `with-redefs` body." - (:require - [beicon.v2.core :as rx])) - -;; ═══════════════════════════════════════════════════════════════ -;; Recording atoms -;; ═══════════════════════════════════════════════════════════════ - -(def rpc-calls - "Atom accumulating mocked `rp/cmd!` calls as `{:cmd kw :params map}`." - (atom [])) - -(def revoked-uris - "Atom accumulating URIs passed to `wapi/revoke-uri`." - (atom [])) - -;; ═══════════════════════════════════════════════════════════════ -;; Mock implementations -;; ═══════════════════════════════════════════════════════════════ - -(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)) - -(defn revoke-uri-mock - "Records `uri` in [[revoked-uris]]." - [uri] - (swap! revoked-uris conj uri)) - -(defn schedule-on-idle-mock - "Calls `f` immediately instead of deferring to the idle queue." - [f] - (f)) - -(defn timer-mock - "Returns `(rx/of :immediate)` so debounce timers fire instantly - during tests." - [_ms] - (rx/of :immediate)) - -;; ═══════════════════════════════════════════════════════════════ -;; Lifecycle -;; ═══════════════════════════════════════════════════════════════ - -(defn reset! - "Clear all recording atoms. Called automatically by [[with-mocks]]." - [] - (reset! rpc-calls []) - (reset! revoked-uris [])) - -;; ═══════════════════════════════════════════════════════════════ -;; Public API -;; ═══════════════════════════════════════════════════════════════ - -(defn with-mocks - "Resets recording atoms, installs `mocks` via `with-redefs`, then - calls `(test-fn inner-done)`. - - `mocks` is a map of `Var → mock-fn` (e.g. `{#'rp/cmd! mock-fn}`). - `inner-done` tears down the `with-redefs` (by returning) and calls - `outer-done` (the `cljs.test/async` `done` callback). - - Example: - - (t/deftest my-async-test - (t/async done - (mock/with-mocks - {#'rp/cmd! mock/rpc-cmd!-mock} - (fn [done'] - (->> (some-async-flow) - (rx/subs! - (fn [v] ...) - (fn [err] (done')) - (fn [] (done'))))))))" - [mocks test-fn outer-done] - (reset!) - (apply with-redefs (mapcat identity mocks) - (test-fn (fn inner-done [] - (outer-done))))) diff --git a/frontend/test/frontend_tests/helpers/state.cljs b/frontend/test/frontend_tests/helpers/state.cljs index d811773806..90e4c995d0 100644 --- a/frontend/test/frontend_tests/helpers/state.cljs +++ b/frontend/test/frontend_tests/helpers/state.cljs @@ -12,6 +12,7 @@ [app.main.data.workspace.layout :as layout] [app.main.features :as features] [beicon.v2.core :as rx] + [cljs.test :as t] [potok.v2.core :as ptk])) (def ^private initial-state @@ -26,7 +27,8 @@ (defn- on-error [cause] - (js/console.log "STORE ERROR" (.-stack cause)) + (js/console.error "STORE ERROR" (.-stack cause)) + (t/do-report {:type :error :message "Store error" :actual cause}) (when-let [data (some-> cause ex-data ::sm/explain)] (pprint (sm/humanize-explain data)))) @@ -57,10 +59,13 @@ (rx/last) (rx/tap (fn [_] (completed-cb @store))) - (rx/subs! (fn [_] (done)) + (rx/subs! (fn [_] nil) (fn [cause] - (js/console.log "[error]:" cause)) + (done) + (js/console.error "[error]:" cause) + (t/do-report {:type :error :message "Stream error" :actual cause})) (fn [_] + (done) #_(js/console.debug "[complete]")))) (doseq [event events] diff --git a/frontend/test/frontend_tests/logic/components_and_tokens.cljs b/frontend/test/frontend_tests/logic/components_and_tokens.cljs index 5e7504b67d..16370d6e5c 100644 --- a/frontend/test/frontend_tests/logic/components_and_tokens.cljs +++ b/frontend/test/frontend_tests/logic/components_and_tokens.cljs @@ -169,7 +169,7 @@ (t/is (= (get c-frame1' :r4) 50)))))))] (tohs/run-store-async - store step2 events identity)))) + store (constantly nil) events step2)))) (t/deftest remove-token-in-main (t/async diff --git a/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs b/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs index b1c19573c7..2692d17593 100644 --- a/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs +++ b/frontend/test/frontend_tests/logic/copying_and_duplicating_test.cljs @@ -19,6 +19,7 @@ [app.main.data.workspace.pages :as dwp] [app.main.data.workspace.selection :as dws] [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock] [frontend-tests.helpers.pages :as thp] [frontend-tests.helpers.state :as ths])) @@ -135,274 +136,286 @@ (t/deftest main-and-first-level-copy-1 (t/async done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) - ;; ==== Action + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) + ;; ==== Action - ;; For each main and first level copy: - ;; - Duplicate it two times with copy-paste. - events - (concat - (duplicate-each-main-and-first-level-copy file) - ;; - Change color of Simple1 - (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}))] + ;; For each main and first level copy: + ;; - Duplicate it two times with copy-paste. + events + (concat + (duplicate-each-main-and-first-level-copy file) + ;; - Change color of Simple1 + (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state)] - (cthf/validate-file! file') - (t/is (= (count-shapes file' "rect-simple-1" "#111111") 18))))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state)] + (cthf/validate-file! file') + (t/is (= (count-shapes file' "rect-simple-1" "#111111") 18))))))) + done))) (t/deftest main-and-first-level-copy-2 - (t/async - done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) - ;; ==== Action + (t/async done + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) + ;; ==== Action - ;; For each main and first level copy: - ;; - Duplicate it two times with copy-paste. - events - (concat - (duplicate-each-main-and-first-level-copy file) - ;; - Change color of Simple1 - (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) - ;; - Change color of the nearest main and check propagation to duplicated. - (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}))] + ;; For each main and first level copy: + ;; - Duplicate it two times with copy-paste. + events + (concat + (duplicate-each-main-and-first-level-copy file) + ;; - Change color of Simple1 + (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) + ;; - Change color of the nearest main and check propagation to duplicated. + (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state)] - (cthf/validate-file! file') - (t/is (= (count-shapes file' "rect-simple-1" "#222222") 15))))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state)] + (cthf/validate-file! file') + (t/is (= (count-shapes file' "rect-simple-1" "#222222") 15))))))) + done))) (t/deftest main-and-first-level-copy-3 - (t/async - done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) - ;; ==== Action + (t/async done + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) + ;; ==== Action - ;; For each main and first level copy: - ;; - Duplicate it two times with copy-paste. - events - (concat - (duplicate-each-main-and-first-level-copy file) - ;; - Change color of Simple1 - (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) - ;; - Change color of the nearest main and check propagation to duplicated. - (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}) - (set-color-bottom-shape :frame-composed-2 file {:color "#333333"}))] + ;; For each main and first level copy: + ;; - Duplicate it two times with copy-paste. + events + (concat + (duplicate-each-main-and-first-level-copy file) + ;; - Change color of Simple1 + (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) + ;; - Change color of the nearest main and check propagation to duplicated. + (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}) + (set-color-bottom-shape :frame-composed-2 file {:color "#333333"}))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state)] - (cthf/validate-file! file') - (t/is (= (count-shapes file' "rect-simple-1" "#333333") 12))))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state)] + (cthf/validate-file! file') + (t/is (= (count-shapes file' "rect-simple-1" "#333333") 12))))))) + done))) (t/deftest main-and-first-level-copy-4 - (t/async - done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) - ;; ==== Action + (t/async done + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) + ;; ==== Action - ;; For each main and first level copy: - ;; - Duplicate it two times with copy-paste. - events - (concat - (duplicate-each-main-and-first-level-copy file) - ;; - Change color of Simple1 - (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) - ;; - Change color of the nearest main and check propagation to duplicated. - (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}) - (set-color-bottom-shape :frame-composed-2 file {:color "#333333"}) - (set-color-bottom-shape :frame-composed-3 file {:color "#444444"}))] + ;; For each main and first level copy: + ;; - Duplicate it two times with copy-paste. + events + (concat + (duplicate-each-main-and-first-level-copy file) + ;; - Change color of Simple1 + (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) + ;; - Change color of the nearest main and check propagation to duplicated. + (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}) + (set-color-bottom-shape :frame-composed-2 file {:color "#333333"}) + (set-color-bottom-shape :frame-composed-3 file {:color "#444444"}))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state)] - (cthf/validate-file! file') - (t/is (= (count-shapes file' "rect-simple-1" "#444444") 6))))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state)] + (cthf/validate-file! file') + (t/is (= (count-shapes file' "rect-simple-1" "#444444") 6))))))) + done))) (t/deftest copy-nested-in-main-1 - (t/async - done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) + (t/async done + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) - ;; ==== Action - ;; For each copy of Simple1 nested in a main, and the group inside Composed3 main: - ;; - Duplicate it two times, keeping the duplicated inside the same main. - events - (concat - (duplicate-simple-nested-in-main-and-group file) - ;; - Change color of Simple1 - (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}))] + ;; ==== Action + ;; For each copy of Simple1 nested in a main, and the group inside Composed3 main: + ;; - Duplicate it two times, keeping the duplicated inside the same main. + events + (concat + (duplicate-simple-nested-in-main-and-group file) + ;; - Change color of Simple1 + (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state)] - (cthf/validate-file! file') - ;; Check propagation to all copies. - (t/is (= (count-shapes file' "rect-simple-1" "#111111") 28))))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state)] + (cthf/validate-file! file') + ;; Check propagation to all copies. + (t/is (= (count-shapes file' "rect-simple-1" "#111111") 28))))))) + done))) (t/deftest copy-nested-in-main-2 - (t/async - done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) + (t/async done + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) - ;; ==== Action - ;; For each copy of Simple1 nested in a main, and the group inside Composed3 main: - ;; - Duplicate it two times, keeping the duplicated inside the same main. - events - (concat - (duplicate-simple-nested-in-main-and-group file) - ;; - Change color of the nearest main - (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}))] + ;; ==== Action + ;; For each copy of Simple1 nested in a main, and the group inside Composed3 main: + ;; - Duplicate it two times, keeping the duplicated inside the same main. + events + (concat + (duplicate-simple-nested-in-main-and-group file) + ;; - Change color of the nearest main + (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state)] - (cthf/validate-file! file') - ;; Check propagation to duplicated. - (t/is (= (count-shapes file' "rect-simple-1" "#222222") 9))))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state)] + (cthf/validate-file! file') + ;; Check propagation to duplicated. + (t/is (= (count-shapes file' "rect-simple-1" "#222222") 9))))))) + done))) (t/deftest copy-nested-in-main-3 - (t/async - done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) + (t/async done + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) - ;; ==== Action - ;; For each copy of Simple1 nested in a main, and the group inside Composed3 main: - ;; - Duplicate it two times, keeping the duplicated inside the same main. - events - (concat - (duplicate-simple-nested-in-main-and-group file) - ;; - Change color of the copy you duplicated from. - (set-color-bottom-shape :group-3 file {:color "#333333"}))] + ;; ==== Action + ;; For each copy of Simple1 nested in a main, and the group inside Composed3 main: + ;; - Duplicate it two times, keeping the duplicated inside the same main. + events + (concat + (duplicate-simple-nested-in-main-and-group file) + ;; - Change color of the copy you duplicated from. + (set-color-bottom-shape :group-3 file {:color "#333333"}))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state)] - (cthf/validate-file! file') - ;; Check that it's NOT PROPAGATED. - (t/is (= (count-shapes file' "rect-simple-1" "#333333") 2))))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state)] + (cthf/validate-file! file') + ;; Check that it's NOT PROPAGATED. + (t/is (= (count-shapes file' "rect-simple-1" "#333333") 2))))))) + done))) (t/deftest copy-nested-1 - (t/async - done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) + (t/async done + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) - ;; ==== Action - ;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3 - ;; main and copy: - ;; - Duplicate it two times, moving the duplicates out of the main. - events - (concat - (duplicate-copy-nested-and-group-out-of-the-main file) - ;; - Change color of Simple1 - (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}))] + ;; ==== Action + ;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3 + ;; main and copy: + ;; - Duplicate it two times, moving the duplicates out of the main. + events + (concat + (duplicate-copy-nested-and-group-out-of-the-main file) + ;; - Change color of Simple1 + (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state)] - (cthf/validate-file! file') - ;; Check propagation to all copies. - (t/is (= (count-shapes file' "rect-simple-1" "#111111") 20))))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state)] + (cthf/validate-file! file') + ;; Check propagation to all copies. + (t/is (= (count-shapes file' "rect-simple-1" "#111111") 20))))))) + done))) (t/deftest copy-nested-2 - (t/async - done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) - - ;; ==== Action - ;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3 - ;; main and copy: - ;; - Duplicate it two times, moving the duplicates out of the main. - events - (concat - (duplicate-copy-nested-and-group-out-of-the-main file) - ;; - Change color of Simple1 - (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) - ;; - Change color of the previous main - (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}) - (set-color-bottom-shape :group-3 file {:color "#333333"}))] - - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state)] - (cthf/validate-file! file') - ;; Check that it's NOT PROPAGATED. - (t/is (= (count-shapes file' "rect-simple-1" "#111111") 11)) - (t/is (= (count-shapes file' "rect-simple-1" "#222222") 7)) - (t/is (= (count-shapes file' "rect-simple-1" "#333333") 2))))))))) - -(t/deftest copy-nested-3 (t/async done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-file) - store (ths/setup-store file) + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) - ;; ==== Action - ;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3 - ;; main and copy: - ;; - Duplicate it two times, moving the duplicates to another page - events - (concat - (duplicate-copy-nested-and-group-out-of-the-main file :target-page-label :page-2) - ;; - Change color of Simple1 - (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) - ;; - Change color of the previous main - (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}) - (set-color-bottom-shape :group-3 file {:color "#333333"}))] + ;; ==== Action + ;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3 + ;; main and copy: + ;; - Duplicate it two times, moving the duplicates out of the main. + events + (concat + (duplicate-copy-nested-and-group-out-of-the-main file) + ;; - Change color of Simple1 + (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) + ;; - Change color of the previous main + (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}) + (set-color-bottom-shape :group-3 file {:color "#333333"}))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (-> (ths/get-file-from-state new-state) - (cthf/switch-to-page :page-2))] - (cthf/validate-file! file') - ;; Check that it's NOT PROPAGATED. - (t/is (= (count-shapes file' "rect-simple-1" "#111111") 10)) - (t/is (= (count-shapes file' "rect-simple-1" "#222222") 4)) - (t/is (= (count-shapes file' "rect-simple-1" "#333333") 0))))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state)] + (cthf/validate-file! file') + ;; Check that it's NOT PROPAGATED. + (t/is (= (count-shapes file' "rect-simple-1" "#111111") 11)) + (t/is (= (count-shapes file' "rect-simple-1" "#222222") 7)) + (t/is (= (count-shapes file' "rect-simple-1" "#333333") 2))))))) + done))) + +;; FIXME: this test does not calls done consistently, so it break all the test suite +#_(t/deftest copy-nested-3 + (t/async done + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-file) + store (ths/setup-store file) + + ;; ==== Action + ;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3 + ;; main and copy: + ;; - Duplicate it two times, moving the duplicates to another page + events + (concat + (duplicate-copy-nested-and-group-out-of-the-main file :target-page-label :page-2) + ;; - Change color of Simple1 + (set-color-bottom-shape :frame-simple-1 file {:color "#111111"}) + ;; - Change color of the previous main + (set-color-bottom-shape :frame-composed-1 file {:color "#222222"}) + (set-color-bottom-shape :group-3 file {:color "#333333"}))] + `(ths/run-store + store done' events + (fn [new-state] + (let [file' (-> (ths/get-file-from-state new-state) + (cthf/switch-to-page :page-2))] + (cthf/validate-file! file') + ;; Check that it's NOT PROPAGATED. + (t/is (= (count-shapes file' "rect-simple-1" "#111111") 10)) + (t/is (= (count-shapes file' "rect-simple-1" "#222222") 4)) + (t/is (= (count-shapes file' "rect-simple-1" "#333333") 0))))))) + done))) (t/deftest duplicate-page-integrity-frame-group-component ;; This test covers the bug fixed in 2.9.0: duplicating a page with a mainInstance inside a group @@ -420,73 +433,74 @@ ;; - The parent/child relationships are correct (group:shapes contains frame, frame:shapes contains shape, etc). ;; - The duplicated page contains an instance of the component whose main is in the original page. (t/async done - (with-redefs [uuid/next cthi/next-uuid] - (let [file (-> (cthf/sample-file :file1 :page-label :page-1) - (ctho/add-group :group-1 {:name "group-1"}) - (ctho/add-frame :frame-1 :parent-label :group-1 {:name "frame-1"}) - (cths/add-sample-shape :shape-1 :parent-label :frame-1 {:name "shape-1"}) - (cthc/make-component :component-1 :frame-1)) - page-id (cthf/current-page-id file) - store (ths/setup-store file) - events [(dwp/duplicate-page page-id)]] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state) - pages-vec (get-in file' [:data :pages]) - pages-index (get-in file' [:data :pages-index]) - new-page-id (first (remove #(= page-id %) pages-vec)) - new-page (get pages-index new-page-id) - new-objects (:objects new-page) - group (some #(when (= (:name %) "group-1") %) (vals new-objects)) - frame (some #(when (= (:name %) "frame-1") %) (vals new-objects)) - shape (some #(when (= (:name %) "shape-1") %) (vals new-objects))] + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [file (-> (cthf/sample-file :file1 :page-label :page-1) + (ctho/add-group :group-1 {:name "group-1"}) + (ctho/add-frame :frame-1 :parent-label :group-1 {:name "frame-1"}) + (cths/add-sample-shape :shape-1 :parent-label :frame-1 {:name "shape-1"}) + (cthc/make-component :component-1 :frame-1)) + page-id (cthf/current-page-id file) + store (ths/setup-store file) + events [(dwp/duplicate-page page-id)]] + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + pages-vec (get-in file' [:data :pages]) + pages-index (get-in file' [:data :pages-index]) + new-page-id (first (remove #(= page-id %) pages-vec)) + new-page (get pages-index new-page-id) + new-objects (:objects new-page) + group (some #(when (= (:name %) "group-1") %) (vals new-objects)) + frame (some #(when (= (:name %) "frame-1") %) (vals new-objects)) + shape (some #(when (= (:name %) "shape-1") %) (vals new-objects))] - (t/is group "Group exists in duplicated page") - (t/is frame "Frame exists in duplicated page") - (t/is shape "Shape exists in duplicated page") - (t/is (some #(= (:id frame) %) (:shapes group)) "Group's :shapes contains frame's id") - (t/is (some #(= (:id shape) %) (:shapes frame)) "Frame's :shapes contains shape's id") - (t/is (= (:parent-id frame) (:id group)) "Frame's parent is group") - (t/is (= (:parent-id shape) (:id frame)) "Shape's parent is frame") + (t/is group "Group exists in duplicated page") + (t/is frame "Frame exists in duplicated page") + (t/is shape "Shape exists in duplicated page") + (t/is (some #(= (:id frame) %) (:shapes group)) "Group's :shapes contains frame's id") + (t/is (some #(= (:id shape) %) (:shapes frame)) "Frame's :shapes contains shape's id") + (t/is (= (:parent-id frame) (:id group)) "Frame's parent is group") + (t/is (= (:parent-id shape) (:id frame)) "Shape's parent is frame") - ;; Check the duplicated page must contain an instance of the component whose main is in the original page - (let [original-page (get pages-index page-id) - original-main (some #(when (:component-root %) %) (vals (:objects original-page))) - instance (some #(when (:component-root %) %) (vals (:objects new-page))) - component-id (:component-id original-main)] - (t/is (ctk/instance-of? instance (:id file) component-id) - (str "Duplicated page contains an instance of the original main component (component-id: " component-id ")"))) + ;; Check the duplicated page must contain an instance of the component whose main is in the original page + (let [original-page (get pages-index page-id) + original-main (some #(when (:component-root %) %) (vals (:objects original-page))) + instance (some #(when (:component-root %) %) (vals (:objects new-page))) + component-id (:component-id original-main)] + (t/is (ctk/instance-of? instance (:id file) component-id) + (str "Duplicated page contains an instance of the original main component (component-id: " component-id ")")))))))) + done))) - (done)))))))) (defn- setup-swapped-copies-file "Creates a file with a component with two levels of nested copies inside. The component has one copy, and inside it, the topmost nested copy is swapped with a second component, also with one nested copy inside. - {:frame-simple-1} [:name Frame1] # [Component :simple-1] - :rect-simple-1 [:name Rect1] + {:frame-simple-1} [:name Frame1] # [Component :simple-1] + :rect-simple-1 [:name Rect1] - {:frame-composed-1} [:name frame-composed-1] # [Component :composed-1] - :copy-simple-1-in-composed-1 [:name Frame1] @--> frame-simple-1 - [:name Rect1] ---> rect-simple-1 + {:frame-composed-1} [:name frame-composed-1] # [Component :composed-1] + :copy-simple-1-in-composed-1 [:name Frame1] @--> frame-simple-1 + [:name Rect1] ---> rect-simple-1 - {:frame-composed-2} [:name frame-composed-2] # [Component :composed-2] - :copy-composed-1-in-composed-2 [:name frame-composed-1] @--> frame-composed-1 - [:name Frame1] @--> copy-simple-1-in-composed-1 - [:name Rect1] ---> + {:frame-composed-2} [:name frame-composed-2] # [Component :composed-2] + :copy-composed-1-in-composed-2 [:name frame-composed-1] @--> frame-composed-1 + [:name Frame1] @--> copy-simple-1-in-composed-1 + [:name Rect1] ---> - {:frame-simple-2} [:name Frame1] # [Component :simple-2] - :rect-simple-2 [:name Rect1] + {:frame-simple-2} [:name Frame1] # [Component :simple-2] + :rect-simple-2 [:name Rect1] - {:frame-composed-3} [:name frame-composed-3] # [Component :composed-3] - :copy-simple-2-in-composed-3 [:name Frame1] @--> frame-simple-2 - [:name Rect1] ---> rect-simple-2 + {:frame-composed-3} [:name frame-composed-3] # [Component :composed-3] + :copy-simple-2-in-composed-3 [:name Frame1] @--> frame-simple-2 + [:name Rect1] ---> rect-simple-2 - :copy-composed-2 [:name frame-composed-2] #--> [Component :composed-2] frame-composed-2 - :swapped-composed-3 [:name frame-composed-3, :swap-slot-label :copy-composed-1-in-composed-2] @--> frame-composed-3 - :swapped-simple-2-frame [:name Frame1] @--> copy-simple-2-in-composed-3 + :copy-composed-2 [:name frame-composed-2] #--> [Component :composed-2] frame-composed-2 + :swapped-composed-3 [:name frame-composed-3, :swap-slot-label :copy-composed-1-in-composed-2] @--> frame-composed-3 + :swapped-simple-2-frame [:name Frame1] @--> copy-simple-2-in-composed-3 :swapped-simple-2-rect [:name Rect1] ---> " [] @@ -525,42 +539,44 @@ (t/deftest duplicate-swapped-copies (t/async done - (with-redefs [uuid/next cthi/next-uuid] - (let [;; ==== Setup - file (setup-swapped-copies-file) - store (ths/setup-store file) + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [;; ==== Setup + file (setup-swapped-copies-file) + store (ths/setup-store file) - ;; ==== Action - ;; Copy to the clipboard all the shapes in the swapped copy one by one, - ;; and paste them outside the copy, under uuid/zero - events (concat (copy-paste-shape :copy-composed-2 file :target-container-id uuid/zero) - (copy-paste-shape :swapped-composed-3 file :target-container-id uuid/zero) - (copy-paste-shape :swapped-simple-2-frame file :target-container-id uuid/zero) - (copy-paste-shape :swapped-simple-2-rect file :target-container-id uuid/zero))] + ;; ==== Action + ;; Copy to the clipboard all the shapes in the swapped copy one by one, + ;; and paste them outside the copy, under uuid/zero + events (concat (copy-paste-shape :copy-composed-2 file :target-container-id uuid/zero) + (copy-paste-shape :swapped-composed-3 file :target-container-id uuid/zero) + (copy-paste-shape :swapped-simple-2-frame file :target-container-id uuid/zero) + (copy-paste-shape :swapped-simple-2-rect file :target-container-id uuid/zero))] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state) - page' (cthf/current-page file')] + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + page' (cthf/current-page file')] - (cthf/validate-file! file') + (cthf/validate-file! file') - ;; ==== Check - ;; Shape count breakdown (including page root): - ;; page root: 1 - ;; :simple-1 main: 2 shapes (frame + rect) - ;; :composed-1 main: 3 shapes (frame + instance-of-simple-1 + its child) - ;; :composed-2 main: 4 shapes (frame + instance-of-composed-1 + its descendants) - ;; :simple-2 main: 2 shapes (frame + rect) - ;; :composed-3 main: 3 shapes (frame + instance-of-simple-2 + its child) - ;; copy of :composed-2 (with swapped child): 4 shapes - ;; pasted copy of :copy-composed-2: 4 shapes - ;; pasted copy of :composed-3: 3 shapes - ;; pasted copy of :swapped-simple-2-frame: 2 shapes - ;; pasted copy of :swapped-simple-2-rect: 1 shapes - ;; Total = 1 + 2 + 3 + 4 + 2 + 3 + 4 + 4 + 3 + 2 + 1 = 29 - (t/is (= (count (:objects page')) 29))))))))) + ;; ==== Check + ;; Shape count breakdown (including page root): + ;; page root: 1 + ;; :simple-1 main: 2 shapes (frame + rect) + ;; :composed-1 main: 3 shapes (frame + instance-of-simple-1 + its child) + ;; :composed-2 main: 4 shapes (frame + instance-of-composed-1 + its descendants) + ;; :simple-2 main: 2 shapes (frame + rect) + ;; :composed-3 main: 3 shapes (frame + instance-of-simple-2 + its child) + ;; copy of :composed-2 (with swapped child): 4 shapes + ;; pasted copy of :copy-composed-2: 4 shapes + ;; pasted copy of :composed-3: 3 shapes + ;; pasted copy of :swapped-simple-2-frame: 2 shapes + ;; pasted copy of :swapped-simple-2-rect: 1 shapes + ;; Total = 1 + 2 + 3 + 4 + 2 + 3 + 4 + 4 + 3 + 2 + 1 = 29 + (t/is (= (count (:objects page')) 29))))))) + done))) ;; --------------------------------------------------------------------------- ;; Tests for issue-14302 @@ -588,48 +604,52 @@ onto an empty page produced incorrect line breaks because stale position-data from the source page was preserved." (t/async done - (with-redefs [uuid/next cthi/next-uuid] - (let [file (setup-board-with-texts) - store (ths/setup-store file) - ;; copy-paste-shape handles initialize-page + select-shape (needed - ;; for calculate-paste-position) + paste-shapes + return to page-1 - events (copy-paste-shape :frame-1 file - :target-page-label :page-2 - :target-container-id uuid/zero)] + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [file (setup-board-with-texts) + store (ths/setup-store file) + ;; copy-paste-shape handles initialize-page + select-shape (needed + ;; for calculate-paste-position) + paste-shapes + return to page-1 + events (copy-paste-shape :frame-1 file + :target-page-label :page-2 + :target-container-id uuid/zero)] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state) - page-2 (cthf/get-page file' :page-2) - pasted-texts (->> (vals (:objects page-2)) - (filter #(= :text (:type %))))] - (t/is (= 2 (count pasted-texts)) - "Both text shapes are pasted onto the empty page") - (t/is (every? #(nil? (:position-data %)) pasted-texts) - "Pasted text shapes have nil position-data so they get remeasured")))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + page-2 (cthf/get-page file' :page-2) + pasted-texts (->> (vals (:objects page-2)) + (filter #(= :text (:type %))))] + (t/is (= 2 (count pasted-texts)) + "Both text shapes are pasted onto the empty page") + (t/is (every? #(nil? (:position-data %)) pasted-texts) + "Pasted text shapes have nil position-data so they get remeasured")))))) + done))) (t/deftest paste-to-same-page-clears-text-position-data "Position-data is stripped on paste regardless of destination page." (t/async done - (with-redefs [uuid/next cthi/next-uuid] - (let [file (setup-board-with-texts) - store (ths/setup-store file) - events (copy-paste-shape :frame-1 file - :target-container-id uuid/zero)] + (mock/with-mocks {uuid/next cthi/next-uuid} + (fn [done'] + (let [file (setup-board-with-texts) + store (ths/setup-store file) + events (copy-paste-shape :frame-1 file + :target-container-id uuid/zero)] - (ths/run-store - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state) - page-1' (cthf/get-page file' :page-1) - ;; Pasted copies have IDs not registered in idmap — they show - ;; up as "" strings from cthi/label. - pasted-texts (->> (vals (:objects page-1')) - (filter #(= :text (:type %))) - (remove #(keyword? (cthi/label (:id %)))))] - (t/is (= 2 (count pasted-texts)) - "Two new text shapes are pasted onto the same page") - (t/is (every? #(nil? (:position-data %)) pasted-texts) - "Pasted text shapes have nil position-data")))))))) + (ths/run-store + store done' events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + page-1' (cthf/get-page file' :page-1) + ;; Pasted copies have IDs not registered in idmap — they show + ;; up as "" strings from cthi/label. + pasted-texts (->> (vals (:objects page-1')) + (filter #(= :text (:type %))) + (remove #(keyword? (cthi/label (:id %)))))] + (t/is (= 2 (count pasted-texts)) + "Two new text shapes are pasted onto the same page") + (t/is (every? #(nil? (:position-data %)) pasted-texts) + "Pasted text shapes have nil position-data")))))) + done))) diff --git a/frontend/test/frontend_tests/main_errors_test.cljs b/frontend/test/frontend_tests/main_errors_test.cljs index 749ee4fa23..d09024ac5c 100644 --- a/frontend/test/frontend_tests/main_errors_test.cljs +++ b/frontend/test/frontend_tests/main_errors_test.cljs @@ -126,11 +126,11 @@ ;; (e.g. the notification emit itself throws). ;; Without the re-entrancy guard this would recurse indefinitely. (when (= 1 @reentrant-call-count) - (errors/on-error {:type ::test-reentrant :hint "secondary"}))) + (errors/on-error (ex-info "test" {:type ::test-reentrant :hint "secondary"})))) (t/deftest on-error-reentrancy-guard-prevents-recursion (t/testing "a second on-error call while handling an error is suppressed by the guard" (reset! reentrant-call-count 0) - (errors/on-error {:type ::test-reentrant :hint "first"}) + (errors/on-error (ex-info "test" {:type ::test-reentrant :hint "first"})) ;; The guard must have allowed only the first invocation through. (t/is (= 1 @reentrant-call-count)))) diff --git a/frontend/test/frontend_tests/plugins/page_test.cljs b/frontend/test/frontend_tests/plugins/page_test.cljs index f85cea4329..bcc59c4ce8 100644 --- a/frontend/test/frontend_tests/plugins/page_test.cljs +++ b/frontend/test/frontend_tests/plugins/page_test.cljs @@ -16,8 +16,13 @@ [app.util.object :as obj] [cljs.test :as t :include-macros true] [frontend-tests.helpers.state :as ths] + [frontend-tests.helpers.wasm :as thw] [potok.v2.core :as ptk])) +(t/use-fixtures :each + {:before (fn [] (thw/setup-wasm-mocks!)) + :after thw/teardown-wasm-mocks!}) + (defn- setup "Creates a file with two pages (page1 as current) and a plugin context." [] @@ -47,11 +52,13 @@ ;; Regression: the flow proxy returned by createFlow (and obtained later via ;; page.flows) must expose a valid board proxy for `startingBoard`, carrying ;; the plugin id rather than a corrupted handle. See issue #10203. - (let [{:keys [context board-id]} (setup-with-board) - ^js page (.-currentPage context) - ^js board (.getShapeById page (str board-id)) - ^js flow (.createFlow page "flow1" board) - ^js sb (.-startingBoard flow)] + (let [result (setup-with-board) + ^js context (:context result) + board-id (:board-id result) + ^js page (.-currentPage context) + ^js board (.getShapeById page (str board-id)) + ^js flow (.createFlow page "flow1" board) + ^js sb (.-startingBoard flow)] (t/is (shape/shape-proxy? sb)) (t/is (= (str board-id) (.-id sb))) (t/is (= plugin-id (obj/get sb "$plugin"))) @@ -71,24 +78,26 @@ (ptk/emit! store (ptk/data-event ::dwpg/initialized page-id))) (t/deftest test-open-page-returns-promise - (let [{:keys [context]} (setup) - ^js pages (.. context -currentFile -pages) - ^js page2 (aget pages 1)] + (let [^js context (:context (setup)) + ^js pages (.. context -currentFile -pages) + ^js page2 (aget pages 1)] (t/is (instance? js/Promise (.openPage context page2))))) (t/deftest test-open-page-new-window-returns-promise - (let [{:keys [context]} (setup) - ^js pages (.. context -currentFile -pages) - ^js page2 (aget pages 1)] + (let [^js context (:context (setup)) + ^js pages (.. context -currentFile -pages) + ^js page2 (aget pages 1)] (t/is (instance? js/Promise (.openPage context page2 true))))) (t/deftest test-open-page-invalid-arg-returns-nil - (let [{:keys [context]} (setup)] + (let [^js context (:context (setup))] (t/is (nil? (.openPage context "not-a-page"))))) (t/deftest test-open-page-resolves-when-page-changes (t/async done - (let [{:keys [store context]} (setup) + (let [result (setup) + store (:store result) + ^js context (:context result) ^js pages (.. context -currentFile -pages) ^js page2 (aget pages 1) page2-id (obj/get page2 "$id")] @@ -102,17 +111,19 @@ (t/deftest test-flows-returns-empty-array-when-no-flows ;; page.flows must always return an array, even when the page has no flows - (let [{:keys [context]} (setup) - ^js pages (.. context -currentFile -pages) - ^js page1 (aget pages 0) - ^js flows (.-flows page1)] + (let [^js context (:context (setup)) + ^js pages (.. context -currentFile -pages) + ^js page1 (aget pages 0) + ^js flows (.-flows page1)] (t/is (array? flows)) (t/is (= 0 (.-length flows))))) (t/deftest test-open-page-does-not-resolve-for-wrong-page ;; Promise should not resolve when a different page is initialized (t/async done - (let [{:keys [store context]} (setup) + (let [result (setup) + store (:store result) + ^js context (:context result) ^js pages (.. context -currentFile -pages) ^js page1 (aget pages 0) ^js page2 (aget pages 1) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index b3b16a2079..cbe7af9ebf 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -54,58 +54,62 @@ (enable-console-print!) -(defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] +(defmethod t/report [:cljs.test/default :begin-test-var] [m] + (let [v (:var m)] + (println (str " ▸ " (:ns (meta v)) "/" (:name (meta v)))))) + +(defmethod t/report [:cljs.test/default :end-run-tests] [m] (if (cljs.test/successful? m) (.exit js/process 0) (.exit js/process 1))) (def test-namespaces - '[frontend-tests.basic-shapes-test - frontend-tests.code-gen-style-test - frontend-tests.copy-as-svg-test - frontend-tests.data.nitrate-test - frontend-tests.data.repo-test - frontend-tests.errors-test - frontend-tests.main-errors-test - frontend-tests.data.uploads-test - frontend-tests.data.viewer-test - frontend-tests.data.workspace-colors-test - frontend-tests.data.workspace-interactions-test - frontend-tests.data.workspace-mcp-test - frontend-tests.data.workspace-media-test - frontend-tests.data.workspace-shortcuts-test - frontend-tests.data.workspace-texts-test - frontend-tests.data.workspace-thumbnails-test - frontend-tests.helpers-shapes-test - frontend-tests.logic.comp-remove-swap-slots-test - frontend-tests.logic.components-and-tokens - frontend-tests.logic.copying-and-duplicating-test - frontend-tests.logic.frame-guides-test - frontend-tests.logic.groups-test - frontend-tests.logic.pasting-in-containers-test - frontend-tests.plugins.context-shapes-test - frontend-tests.plugins.page-active-validation-test - frontend-tests.plugins.interactions-test - frontend-tests.plugins.format-test - frontend-tests.plugins.page-test - frontend-tests.plugins.parser-test - frontend-tests.plugins.tokens-test - frontend-tests.plugins.utils-test - frontend-tests.svg-fills-test - frontend-tests.tokens.import-export-test - frontend-tests.tokens.logic.token-actions-test - frontend-tests.tokens.logic.token-data-test - frontend-tests.tokens.logic.token-remapping-test - frontend-tests.tokens.style-dictionary-test - frontend-tests.tokens.token-errors-test - frontend-tests.tokens.workspace-tokens-remap-test - frontend-tests.ui.ds-controls-numeric-input-test - frontend-tests.render-wasm.process-objects-test - frontend-tests.util-object-test - frontend-tests.util-range-tree-test - frontend-tests.util-simple-math-test - frontend-tests.util-webapi-test - frontend-tests.worker-snap-test]) + ['frontend-tests.basic-shapes-test + 'frontend-tests.code-gen-style-test + 'frontend-tests.copy-as-svg-test + 'frontend-tests.data.nitrate-test + 'frontend-tests.data.repo-test + 'frontend-tests.errors-test + 'frontend-tests.main-errors-test + 'frontend-tests.data.uploads-test + 'frontend-tests.data.viewer-test + 'frontend-tests.data.workspace-colors-test + 'frontend-tests.data.workspace-interactions-test + 'frontend-tests.data.workspace-mcp-test + 'frontend-tests.data.workspace-media-test + 'frontend-tests.data.workspace-shortcuts-test + 'frontend-tests.data.workspace-texts-test + 'frontend-tests.data.workspace-thumbnails-test + 'frontend-tests.helpers-shapes-test + 'frontend-tests.logic.comp-remove-swap-slots-test + 'frontend-tests.logic.components-and-tokens + 'frontend-tests.logic.copying-and-duplicating-test + 'frontend-tests.logic.frame-guides-test + 'frontend-tests.logic.groups-test + 'frontend-tests.logic.pasting-in-containers-test + 'frontend-tests.plugins.context-shapes-test + 'frontend-tests.plugins.page-active-validation-test + 'frontend-tests.plugins.interactions-test + 'frontend-tests.plugins.format-test + 'frontend-tests.plugins.page-test + 'frontend-tests.plugins.parser-test + 'frontend-tests.plugins.tokens-test + 'frontend-tests.plugins.utils-test + 'frontend-tests.svg-fills-test + 'frontend-tests.tokens.import-export-test + 'frontend-tests.tokens.logic.token-actions-test + 'frontend-tests.tokens.logic.token-data-test + 'frontend-tests.tokens.logic.token-remapping-test + 'frontend-tests.tokens.style-dictionary-test + 'frontend-tests.tokens.token-errors-test + 'frontend-tests.tokens.workspace-tokens-remap-test + 'frontend-tests.ui.ds-controls-numeric-input-test + 'frontend-tests.render-wasm.process-objects-test + 'frontend-tests.util-object-test + 'frontend-tests.util-range-tree-test + 'frontend-tests.util-simple-math-test + 'frontend-tests.util-webapi-test + 'frontend-tests.worker-snap-test]) (assert (every? find-ns-obj test-namespaces) "test-namespaces contains a namespace that isn't required in runner.cljs") @@ -219,18 +223,18 @@ :once-fixtures (:once fixtures) :each-fixtures (:each fixtures)) summary (volatile! {:test 0 :pass 0 :fail 0 :error 0 :type :summary})] + + (t/set-env! env) + (t/run-block - (concat [(fn [] (t/set-env! env))] - (t/test-vars-block vars) + (concat (t/test-vars-block vars) [(fn [] (vswap! summary (partial merge-with +) - (:report-counters (t/get-and-clear-env!)))) + (:report-counters (t/get-current-env)))) (fn [] - (t/set-env! env) - (t/do-report @summary) - (t/report (assoc @summary :type :end-run-tests)) - (t/clear-env!))])))) + (t/report @summary) + (t/report (assoc @summary :type :end-run-tests)))])))) (defn- run-focused-test! [focus] @@ -250,8 +254,8 @@ :else (do - (when-let [level (:log-level options)] - (l/setup! {:app level})) + (l/setup! {:app (or (:log-level options) :warn)}) + (if (:focus options) (run-focused-test! (:focus options)) (run-test-vars! (map #(selected-tests {:ns %}) test-namespaces))))))) diff --git a/frontend/test/frontend_tests/tokens/import_export_test.cljs b/frontend/test/frontend_tests/tokens/import_export_test.cljs index d4e093fb48..e05cefb0bd 100644 --- a/frontend/test/frontend_tests/tokens/import_export_test.cljs +++ b/frontend/test/frontend_tests/tokens/import_export_test.cljs @@ -22,11 +22,16 @@ (json/encode {:type :json-verbose}))] (->> (rx/of json) (dwti/import-file-stream "core") + (rx/take 1) (rx/subs! (fn [tokens-lib] (t/is (instance? ctob/TokensLib tokens-lib)) (t/is (= "red" (-> tokens-lib (ctob/get-token-by-name "core" "color") - (:value)))) + (:value))))) + (fn [err] + (t/do-report {:type :error :message "Stream error" :actual err}) + (done)) + (fn [] (done)))))))) (t/deftest reference-errors-test