Add improvements for frontend tests (#10380)

This commit is contained in:
Andrey Antukh 2026-06-23 11:21:53 +02:00 committed by GitHub
parent 07de0e92d5
commit f967a0fc83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 632 additions and 553 deletions

View File

@ -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!

View File

@ -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'\"",

View File

@ -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

View File

@ -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))

View File

@ -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 [])

View File

@ -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 []))))

View File

@ -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)))))

View File

@ -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]

View File

@ -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

View File

@ -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
<no-label #000894> [: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
<no-label #000894> [: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
<no-label #000899> [:name Frame1] @--> copy-simple-1-in-composed-1
<no-label #00089a> [:name Rect1] ---> <no-label #000894>
{:frame-composed-2} [:name frame-composed-2] # [Component :composed-2]
:copy-composed-1-in-composed-2 [:name frame-composed-1] @--> frame-composed-1
<no-label #000899> [:name Frame1] @--> copy-simple-1-in-composed-1
<no-label #00089a> [:name Rect1] ---> <no-label #000894>
{: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
<no-label #0008a4> [: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
<no-label #0008a4> [: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] ---> <no-label #0008a4>
"
[]
@ -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 "<no-label …>" 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 "<no-label …>" 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)))

View File

@ -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))))

View File

@ -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)

View File

@ -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)))))))

View File

@ -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