Sfoglia il codice sorgente

Fix plugin install bugs and add plugin-config tests

- Add validation for plugins.edn with malli
- Add bb task using malli schema
- Add first async test that interacts with frontend.fs
- Add test helpers to make future async testing easier
Gabriel Horner 3 anni fa
parent
commit
c8cda90373

+ 2 - 0
.clj-kondo/config.edn

@@ -98,6 +98,8 @@
            frontend.modules.outliner.datascript/auto-transact! clojure.core/let
            frontend.namespaces/import-vars potemkin/import-vars
            ;; src/test
+           frontend.test.helper/deftest-async clojure.test/deftest
+           frontend.test.helper/with-reset cljs.test/async
            frontend.react/defc clojure.core/defn}
  :skip-comments true
  :output {:progress true}}

+ 5 - 0
bb.edn

@@ -3,6 +3,8 @@
  {org.babashka/spec.alpha
   {:git/url "https://github.com/babashka/spec.alpha"
    :sha "1a841c4cc1d4f6dab7505a98ed2d532dd9d56b78"}
+  metosin/malli
+  {:mvn/version "0.8.9"}
   logseq/bb-tasks
   #_{:local/root "../bb-tasks"}
   {:git/url "https://github.com/logseq/bb-tasks"
@@ -61,6 +63,9 @@
   dev:validate-local-storage
   logseq.tasks.spec/validate-local-storage
 
+  dev:validate-plugins-edn
+  logseq.tasks.malli/validate-plugins-edn
+
   dev:lint
   logseq.tasks.dev/lint
 

+ 2 - 1
deps.edn

@@ -28,7 +28,8 @@
   camel-snake-kebab/camel-snake-kebab   {:mvn/version "0.4.2"}
   instaparse/instaparse                 {:mvn/version "1.4.10"}
   org.clojars.mmb90/cljs-cache          {:mvn/version "0.1.4"}
-  logseq/graph-parser                   {:local/root "deps/graph-parser"}}
+  logseq/graph-parser                   {:local/root "deps/graph-parser"}
+  metosin/malli                         {:mvn/version "0.8.9"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}

+ 10 - 7
docs/dev-practices.md

@@ -153,15 +153,18 @@ aren't readable.
 
 ## Data validation and generation
 
-We currently use [spec](https://github.com/clojure/spec.alpha) for data
-validation (and generation someday). We may switch to
-[malli](https://github.com/metosin/malli) if we need to datafy our data models
-at some point.
+We use both [spec](https://github.com/clojure/spec.alpha) and [malli](https://github.com/metosin/malli) for data validation and (and generation someday). malli has the advantage that its schema
+is data and can be used for additional purposes. See plugin-config for an example.
 
 Specs should go under `src/main/frontend/spec/` and be compatible with clojure
-and clojurescript. See `frontend.spec.storage` for an example. By following
-these conventions, specs should also be usable by babashka. This is helpful as it
-allows for third party tools to be written with logseq's data model.
+and clojurescript. See `frontend.spec.storage` for an example.
+
+Malli schemas should go under `src/main/frontend/schema/` and be compatible with clojure
+and clojurescript. See `frontend.schema.handler.plugin-config` for an example.
+
+By following these conventions, these should also be usable by babashka. This is
+helpful as it allows for third party tools to be written with logseq's data
+model.
 
 ## Development Tools
 

+ 19 - 0
scripts/src/logseq/tasks/malli.clj

@@ -0,0 +1,19 @@
+(ns logseq.tasks.malli
+  "Malli related tasks"
+  (:require [malli.core :as m]
+            [malli.error :as me]
+            [frontend.schema.handler.plugin-config :as plugin-config-schema]
+            [clojure.pprint :as pprint]
+            [clojure.edn :as edn]))
+
+(defn validate-plugins-edn
+  [file]
+  (if-let [errors (->> file
+                       slurp
+                       edn/read-string
+                       (m/explain plugin-config-schema/Plugins-edn)
+                       me/humanize)]
+    (do
+      (println "Found errors:")
+      (pprint/pprint errors))
+    (println "Valid!")))

+ 44 - 26
src/electron/electron/plugin.cljs

@@ -32,39 +32,57 @@
 ;; Zipball https://api.github.com/repos/{owner}/{repo}/zipball
 
 (defn- fetch-release-asset
-  [{:keys [repo theme]} url-suffix]
+  [{:keys [repo theme]} url-suffix {:keys [response-transform]
+                                    :or {response-transform identity}}]
   (p/catch
-    (p/let [repo (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
-            api #(str "https://api.github.com/repos/" repo "/" %)
-            endpoint (api url-suffix)
-            ^js res (fetch endpoint)
-            res (.json res)
-            _ (debug "[Release URL] " endpoint)
-            res (bean/->clj res)
-            version (:tag_name res)
-            asset (first (filter #(string/ends-with? (:name %) ".zip") (:assets res)))]
-
-      [(if (and (nil? asset) theme)
-         (if-let [zipball (:zipball_url res)]
-           zipball
-           (api "zipball"))
-         asset)
-       version
-       (:body res)])
-
-    (fn [^js e]
-      (debug e)
-      (throw (js/Error. [:release-channel-issue (.-message e)])))))
+   (p/let [repo (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
+           api #(str "https://api.github.com/repos/" repo "/" %)
+           endpoint (api url-suffix)
+           ^js res (fetch endpoint)
+           res (response-transform res)
+           res (.json res)
+           _ (debug "[Release URL] " endpoint)
+           res (bean/->clj res)
+           version (:tag_name res)
+           asset (first (filter #(string/ends-with? (:name %) ".zip") (:assets res)))]
+
+          [(if (and (nil? asset) theme)
+             (if-let [zipball (:zipball_url res)]
+               zipball
+               (api "zipball"))
+             asset)
+           version
+           (:body res)])
+
+   (fn [^js e]
+     (debug e)
+     (throw (js/Error. [:release-channel-issue (.-message e)])))))
 
 (defn fetch-latest-release-asset
   "Fetches latest release, normally when user clicks to install or update a plugin"
   [item]
-  (fetch-release-asset item "releases/latest"))
+  (fetch-release-asset item "releases/latest" {}))
 
 (defn fetch-specific-release-asset
-  "Fetches a specific release asset, normally when installing specific versions from plugins.edn"
-  [{:keys [version] :as item}]
-  (fetch-release-asset item (str "releases/tags/" version)))
+  "Fetches a specific release asset, normally when installing specific versions
+  from plugins.edn. If a release does not exist, it falls back to fetching the
+  latest release for a plugin. This is done for unusual plugins where the
+  package.json version does not match the git tagged version e.g.
+  https://github.com/hkgnp/logseq-osmmaps-plugin has respective values of 1.5
+  and v1.5."
+  [{:keys [version repo] :as item}]
+  (fetch-release-asset item
+                       (str "releases/tags/" version)
+                       {:response-transform
+                        (fn [res]
+                          (if (= 404 (.-status res))
+                            ;; Fall back to fetching the latest For these rare
+                            ;; cases, previous logseq versions did not store the
+                            ;; plugin's git tag required to correctly install it
+                            (let [repo' (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
+                                  api #(str "https://api.github.com/repos/" repo' "/" %)]
+                              (fetch (api "releases/latest")))
+                            res))}))
 
 (defn download-asset-zip
   [{:keys [id repo title author description effect sponsors]} dl-url dl-version dot-extract-to]

+ 2 - 3
src/main/frontend/components/plugins.cljs

@@ -806,9 +806,8 @@
    [:h1.mb-4.text-2xl.p-1 "Install plugins from plugins.edn"]
    (if (seq plugins)
      [:div
-      [:div.mb-2.text-xl (util/format "The following %s plugin(s) will replace your %s plugin(s):"
-                                      (count (:install plugins))
-                                      (count (:uninstall plugins)))]
+      [:div.mb-2.text-xl (util/format "The following %s plugin(s) will replace your plugins:"
+                                      (count (:install plugins)))]
       ;; lists
       [:ul
        (for [it (:install plugins)

+ 9 - 10
src/main/frontend/handler/plugin.cljs

@@ -201,22 +201,21 @@
                            (if (installed? id)
                              (when-let [^js pl (get-plugin-inst id)] ;; update
                                (p/then
-                                 (.reload pl)
-                                 #(do
-                                    ;;(if theme (select-a-plugin-theme id))
-                                    (notification/show!
-                                      (str (t :plugin/update) (t :plugins) ": " name " - " (.-version (.-options pl))) :success)
-                                    (state/consume-updates-coming-plugin payload true))))
+                                (.reload pl)
+                                #(do
+                                   ;;(if theme (select-a-plugin-theme id))
+                                   (notification/show!
+                                    (str (t :plugin/update) (t :plugins) ": " name " - " (.-version (.-options pl))) :success)
+                                   (state/consume-updates-coming-plugin payload true))))
 
                              (do                            ;; register new
                                (p/then
                                 (js/LSPluginCore.register (bean/->js {:key id :url dst}))
                                 (fn [] (when theme (js/setTimeout #(select-a-plugin-theme id) 300))))
                                (plugin-config/add-or-update-plugin
-                                {:id id
-                                 :name (:name payload)
-                                 :version (:installed-version payload)
-                                 :repo (:repo payload)})
+                                (assoc payload
+                                       :version (:installed-version payload)
+                                       :name name))
                                (notification/show!
                                 (str (t :plugin/installed) (t :plugins) ": " name) :success)))))
 

+ 35 - 12
src/main/frontend/handler/plugin_config.cljs

@@ -7,21 +7,30 @@ This component depends on TODO"
             [borkdude.rewrite-edn :as rewrite]
             [frontend.fs :as fs]
             [frontend.state :as state]
+            [frontend.handler.notification :as notification]
             [electron.ipc :as ipc]
             [clojure.edn :as edn]
             [clojure.set :as set]
-            [clojure.pprint :as pprint]))
+            [clojure.pprint :as pprint]
+            [malli.core :as m]
+            [malli.error :as me]
+            [frontend.schema.handler.plugin-config :as plugin-config-schema]
+            [lambdaisland.glogi :as log]))
 
 (defn- plugin-config-path
   []
   (path/join @global-config-handler/root-dir "plugins.edn"))
 
+(def common-plugin-keys
+  "Vec of plugin keys to store in plugins.edn and to compare with installed-plugins state"
+  (->> plugin-config-schema/Plugins-edn last rest (mapv first)))
+
 (defn add-or-update-plugin
   [{:keys [id] :as plugin}]
   (p/let [content (fs/read-file "" (plugin-config-path))
           updated-content (-> content
                               rewrite/parse-string
-                              (rewrite/assoc (keyword id) (dissoc plugin :id))
+                              (rewrite/assoc (keyword id) (select-keys plugin common-plugin-keys))
                               str)]
          ;; fs protocols require repo and dir when they aren't necessary. For this component,
          ;; neither is needed so these are nil and blank respectively
@@ -36,9 +45,9 @@ This component depends on TODO"
 (defn- create-plugin-config-file-if-not-exists
   []
   (let [content (-> (:plugin/installed-plugins @state/state)
-                    (update-vals #(select-keys % [:name :version :repo]))
-                     pprint/pprint
-                     with-out-str)]
+                    (update-vals #(select-keys % common-plugin-keys))
+                    pprint/pprint
+                    with-out-str)]
     (fs/create-if-not-exists nil @global-config-handler/root-dir (plugin-config-path) content)))
 
 (defn- determine-plugins-to-change
@@ -47,7 +56,7 @@ returns map of plugins to install and uninstall"
   [installed-plugins edn-plugins]
   (let [installed-plugins-set (->> installed-plugins
                                    vals
-                                   (map #(assoc (select-keys % [:name :version :repo])
+                                   (map #(assoc (select-keys % common-plugin-keys)
                                                 :id (keyword (:id %))))
                                    set)
         edn-plugins-set (->> edn-plugins
@@ -57,16 +66,28 @@ returns map of plugins to install and uninstall"
       {}
       {:install (mapv #(assoc % :plugin-action "install")
                       (set/difference edn-plugins-set installed-plugins-set))
-       :uninstall (set/difference installed-plugins-set edn-plugins-set)})))
+       :uninstall (vec (set/difference installed-plugins-set edn-plugins-set))})))
 
 (defn open-sync-modal
   []
   (state/pub-event! [:go/plugins])
-  (p/let [edn-plugins (fs/read-file "" (plugin-config-path))
-          plugins-to-change (determine-plugins-to-change
-                             (:plugin/installed-plugins @state/state)
-                             (edn/read-string edn-plugins))]
-         (state/pub-event! [:go/plugins-from-file plugins-to-change])))
+  (p/catch
+   (p/let [edn-plugins* (fs/read-file "" (plugin-config-path))
+           edn-plugins (edn/read-string edn-plugins*)]
+          (if-let [errors (->> edn-plugins (m/explain plugin-config-schema/Plugins-edn) me/humanize)]
+            (do
+              (notification/show! "Invalid plugins.edn provided. See javascript console for specific errors"
+                                  :error)
+              (log/error :plugin-edn-errors errors))
+            (let [plugins-to-change (determine-plugins-to-change
+                                     (:plugin/installed-plugins @state/state)
+                                     edn-plugins)]
+              (state/pub-event! [:go/plugins-from-file plugins-to-change]))))
+   (fn [e]
+     (if (= :reader-exception (:type (ex-data e)))
+       (notification/show! "Malformed plugins.edn provided. Please check the file has correct edn syntax."
+                           :error)
+       (log/error :unexpected-error e)))))
 
 ;; TODO: Extract from handler.plugin
 (defn installed?
@@ -89,8 +110,10 @@ returns map of plugins to install and uninstall"
 
 (defn update-plugins
   [plugins]
+  (log/info :uninstall-plugins (:uninstall plugins))
   (doseq [plugin (:uninstall plugins)]
     (js/LSPluginCore.unregister (name (:id plugin))))
+  (log/info :install-plugins (:install plugins))
   (doseq [plugin (:install plugins)]
     (install-marketplace-plugin plugin)))
 

+ 5 - 1
src/main/frontend/idb.cljs

@@ -4,6 +4,7 @@
             [clojure.string :as string]
             [frontend.config :as config]
             [frontend.storage :as storage]
+            [frontend.util :as util]
             [goog.object :as gobj]
             [promesa.core :as p]))
 
@@ -13,7 +14,10 @@
 ;; To maintain backward compatibility
 
 
-(defonce store (Store. "localforage" "keyvaluepairs" 2))
+;; store is unused and fails async tests so disable them in tests
+(if util/node-test?
+  (def store nil)
+  (defonce store (Store. "localforage" "keyvaluepairs" 2)))
 
 (defn clear-idb!
   []

+ 24 - 0
src/main/frontend/schema/handler/plugin_config.cljc

@@ -0,0 +1,24 @@
+(ns ^:bb-compatible frontend.schema.handler.plugin-config)
+
+(def Plugins-edn
+  [:map-of
+   [:and
+    ;; Use qualified and call name on it as :keyword sometimes generated an incompatible
+    ;; suffix e.g. :/.
+    {:gen/schema :qualified-keyword
+     :gen/fmap '(fn [x] (keyword (str "id-" (name x))))}
+    :keyword]
+   ; The plugin keys should not be changed between releases without a migration plan
+   ; for existing config files
+   [:map
+    [:name
+     [:and {:gen/fmap '(partial str "Name ")}
+      string?]]
+    [:version
+     [:and
+      {:gen/fmap '(fn [_] (apply str (interpose "." (repeatedly 3 (fn [] (rand-int 10))))))}
+      string?]]
+    [:repo
+     [:and {:gen/fmap '(partial str "github-user/")}
+      string?]]
+    [:theme boolean?]]])

+ 13 - 0
src/test/frontend/fs/test_node.cljs

@@ -0,0 +1,13 @@
+(ns frontend.fs.test-node
+  "Test implementation of fs protocol for node.js"
+  (:require [frontend.fs.protocol :as protocol]
+            ["fs" :as fs-node]))
+
+;; Most protocol fns are not defined. Define them as needed for tests
+(defrecord NodeTestfs
+  []
+  protocol/Fs
+  (read-file [_this _dir path _options]
+             (str (fs-node/readFileSync path)))
+  (write-file! [_this _repo _dir path content _opts]
+               (fs-node/writeFileSync path content)))

+ 129 - 0
src/test/frontend/handler/plugin_config_test.cljs

@@ -0,0 +1,129 @@
+(ns frontend.handler.plugin-config-test
+  (:require [clojure.test :refer [is use-fixtures testing deftest]]
+            [frontend.test.helper :as helper :include-macros true :refer [deftest-async]]
+            [frontend.test.fixtures :as fixtures]
+            [frontend.handler.plugin-config :as plugin-config]
+            [frontend.handler.global-config :as global-config-handler]
+            [frontend.schema.handler.plugin-config :as plugin-config-schema]
+            ["fs" :as fs-node]
+            ["path" :as path]
+            [clojure.edn :as edn]
+            [malli.generator :as mg]
+            [promesa.core :as p]
+            [clojure.string :as string]
+            [frontend.handler.notification :as notification]))
+
+(use-fixtures :once fixtures/redef-get-fs)
+
+(deftest-async add-or-update-plugin
+  (let [dir (helper/create-tmp-dir)
+        plugins-file (path/join dir "plugins.edn")
+        plugin-to-add {:id :foo :name "Foo" :repo "some-user/foo" :version "v0.9.0"}
+        body (pr-str (mg/generate plugin-config-schema/Plugins-edn {:size 10}))]
+    (fs-node/writeFileSync plugins-file body)
+    (reset! global-config-handler/root-dir dir)
+
+    (->
+     (p/do!
+      (plugin-config/add-or-update-plugin plugin-to-add)
+      (is (= (dissoc plugin-to-add :id)
+             (:foo (edn/read-string (str (fs-node/readFileSync plugins-file)))))))
+
+     (.finally
+      (fn []
+        (fs-node/unlinkSync plugins-file)
+        (fs-node/rmdirSync dir))))))
+
+(deftest-async remove-plugin
+  (let [dir (helper/create-tmp-dir)
+        plugins-file (path/join dir "plugins.edn")
+        ;; use seed to consistently generate 5 plugins
+        ;; if we want more randomness we could look into gen/such-that
+        plugins (mg/generate plugin-config-schema/Plugins-edn {:size 5 :seed 1})
+        some-plugin-id (first (keys plugins))]
+    (fs-node/writeFileSync plugins-file (pr-str plugins))
+    (reset! global-config-handler/root-dir dir)
+
+    (->
+     (p/do!
+      (plugin-config/remove-plugin some-plugin-id)
+      (is (= nil
+             (get (edn/read-string (str (fs-node/readFileSync plugins-file)))
+                  some-plugin-id))))
+
+     (.finally
+      (fn []
+        (fs-node/unlinkSync plugins-file)
+        (fs-node/rmdirSync dir))))))
+
+(deftest-async open-sync-modal-malformed-edn
+  (let [dir (helper/create-tmp-dir)
+        plugins-file (path/join dir "plugins.edn")
+        error-message (atom nil)]
+    (fs-node/writeFileSync plugins-file "{:id {}")
+    (reset! global-config-handler/root-dir dir)
+
+    (helper/with-reset reset
+      [notification/show! (fn [msg _] (reset! error-message msg))]
+      (->
+       (p/do!
+        (plugin-config/open-sync-modal)
+        (is (string/starts-with? @error-message "Malformed plugins.edn")
+            "User sees correct notification"))
+       (p/finally (fn []
+                    (reset)
+                    (fs-node/unlinkSync plugins-file)
+                    (fs-node/rmdirSync dir)))))))
+
+(deftest-async open-sync-modal-invalid-edn
+  (let [dir (helper/create-tmp-dir)
+        plugins-file (path/join dir "plugins.edn")
+        error-message (atom nil)]
+    ;; Missing a couple plugin keys
+    (fs-node/writeFileSync plugins-file (pr-str {:id {:theme true :repo "user/repo"}}))
+    (reset! global-config-handler/root-dir dir)
+
+    (helper/with-reset reset
+      [notification/show! (fn [msg _] (reset! error-message msg))]
+      (->
+       (p/do!
+        (plugin-config/open-sync-modal)
+        (is (string/starts-with? @error-message "Invalid plugins.edn")
+            "User sees correct notification"))
+       (p/finally (fn []
+                    (reset)
+                    (fs-node/unlinkSync plugins-file)
+                    (fs-node/rmdirSync dir)))))))
+
+(defn- installed-plugins->edn-plugins
+  "Converts installed plugins state to edn.plugins format"
+  [m]
+  (update-vals m #(dissoc % :id)))
+
+;; These tests check the full return value as most of it is important for correct
+;; install and uninstall
+(deftest determine-plugins-to-change
+  (testing "no changes to make"
+    (let [plugins {:foo {:id :foo :name "Foo" :repo "some-user/foo" :version "v0.9.0"}
+                   :bar {:id :bar :name "Bar" :repo "some-user/bar" :version "v0.1.0"}}]
+      (is (= {} (#'plugin-config/determine-plugins-to-change
+                  plugins
+                  (installed-plugins->edn-plugins plugins))))))
+
+  (testing "differing versions are uninstalled and installed"
+    (let [plugins {:bar {:id :bar :name "Bar" :repo "some-user/bar" :version "v0.1.0"}}]
+      (is (= {:uninstall [(:bar plugins)]
+              :install [(assoc (:bar plugins) :version "v1.0.0" :plugin-action "install")]}
+             (#'plugin-config/determine-plugins-to-change
+               plugins
+               (installed-plugins->edn-plugins (assoc-in plugins [:bar :version] "v1.0.0")))))))
+
+  (testing "replaced plugins are uninstalled and new plugins are installed"
+    (let [plugins {:foo {:id :foo :name "Foo" :repo "some-user/foo" :version "v0.9.0"}
+                   :bar {:id :bar :name "Bar" :repo "some-user/bar" :version "v0.1.0"}}
+          new-plugin {:id :baz :name "Baz" :repo "some-user/baz" :version "v0.5.0"}]
+      (is (= {:uninstall [(:foo plugins)]
+              :install [(assoc new-plugin :plugin-action "install")]}
+             (#'plugin-config/determine-plugins-to-change
+               plugins
+               (-> plugins (dissoc :foo) (assoc :baz new-plugin) installed-plugins->edn-plugins)))))))

+ 14 - 1
src/test/frontend/test/fixtures.cljs

@@ -4,8 +4,11 @@
             [logseq.db.schema :as db-schema]
             [frontend.db.conn :as conn]
             [frontend.db.react :as react]
+            [frontend.fs.test-node :as test-node]
+            [frontend.fs :as fs]
             [frontend.state :as state]
-            [frontend.test.helper :as helper]))
+            [frontend.test.helper :as helper]
+            [cljs.test :refer [async]]))
 
 (defn load-test-env
   [f]
@@ -31,3 +34,13 @@
     (reset-datascript repo)
     (let [r (f)]
       (reset-datascript repo) r)))
+
+(let [get-fs-fn (atom nil)]
+  (def redef-get-fs
+    "Redef fs/get-fs to an implementation that is valid for node tests"
+    {:before (fn []
+               (async done
+                      (reset! get-fs-fn fs/get-fs)
+                      (set! fs/get-fs (constantly (test-node/->NodeTestfs)))
+                      (done)))
+     :after (fn [] (set! fs/get-fs @get-fs-fn))}))

+ 41 - 0
src/test/frontend/test/helper.clj

@@ -6,3 +6,44 @@
      (frontend.state/set-config! repo# ~config)
      ~@body
      (frontend.state/set-config! repo# nil)))
+
+;; Copied from https://github.com/babashka/nbb/blob/e5d84b0fac59774f5d7a4a9e807240cce04bf252/test/nbb/test_macros.clj
+;; This macro ensures an async test's uncaught exception is caught and correctly
+;; errors the test suite. It also handles calling async and done
+(defmacro deftest-async [name opts & body]
+  (let [[opts body]
+        (if (map? opts)
+          [opts body]
+          [nil (cons opts body)])]
+    `(cljs.test/deftest ~name
+       ~@(when-let [pre (:before opts)]
+           [pre])
+       (cljs.test/async
+        ~'done
+        (-> (do ~@body)
+            (.catch (fn [err#]
+                      (cljs.test/is (= 1 0) (str err# (.-stack err#)))))
+            (.finally
+             (fn []
+               ~@(when-let [post (:after opts)]
+                   [post])
+               (~'done))))))))
+
+;; Copied from https://clojurians.slack.com/archives/C03S1L9DN/p1631221014001900?thread_ts=1631219838.001500&cid=C03S1L9DN
+(defmacro with-reset
+  "Like cljs.core/with-redefs, but bindings persist until the `reset` fn is
+ called, allowing bindings to be used in async contexts."
+  [reset bindings & body]
+  ;; code adapted from https://cljs.github.io/api/cljs.core/with-redefs
+  (let [names (take-nth 2 bindings)
+        vals (take-nth 2 (drop 1 bindings))
+        orig-val-syms (map (comp gensym #(str % "-orig-val__") name) names)
+        temp-val-syms (map (comp gensym #(str % "-temp-val__") name) names)
+        binds (map vector names temp-val-syms)
+        redefs (reverse (map vector names orig-val-syms))
+        bind-value (fn [[k v]] (list 'set! k v))]
+    `(let [~@(interleave orig-val-syms names)
+           ~@(interleave temp-val-syms vals)
+           ~reset #(do ~@(map bind-value redefs))]
+       ~@(map bind-value binds)
+       ~@body)))

+ 7 - 3
src/test/frontend/test/helper.cljs

@@ -1,9 +1,10 @@
 (ns frontend.test.helper
   "Common helper fns for tests"
   (:require [frontend.handler.repo :as repo-handler]
-            [frontend.db.persist :as db-persist]
             [frontend.state :as state]
-            [frontend.db.conn :as conn]))
+            [frontend.db.conn :as conn]
+            ["path" :as path]
+            ["fs" :as fs-node]))
 
 (defonce test-db "test-db")
 
@@ -17,9 +18,12 @@
 
 (defn clear-current-repo []
   (let [current-repo (state/get-current-repo)]
-    (db-persist/delete-graph! current-repo)
     (destroy-test-db!)
     (conn/start! current-repo)))
 
 (defn load-test-files [files]
   (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false :verbose false}))
+
+(defn create-tmp-dir
+  []
+  (fs-node/mkdtempSync (path/join "tmp" "unit-test-")))