mirror of
https://github.com/logseq/logseq.git
synced 2026-06-01 19:01:22 +00:00
Merge master into enhance/i18n
This commit is contained in:
35
src/test/frontend/components/property/property_test.cljs
Normal file
35
src/test/frontend/components/property/property_test.cljs
Normal file
@@ -0,0 +1,35 @@
|
||||
(ns frontend.components.property.property-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[frontend.components.property :as property-component]))
|
||||
|
||||
(deftest sanitize-property-values-for-display-filters-recycled-entity-values-test
|
||||
(let [active-value {:db/id 101
|
||||
:block/title "Active"}
|
||||
recycled-value {:db/id 102
|
||||
:block/title "Recycled"
|
||||
:logseq.property/deleted-at 1}
|
||||
{:keys [properties recycled-only-property-ids]}
|
||||
(#'property-component/sanitize-property-values-for-display
|
||||
{:user.property/node #{active-value recycled-value}
|
||||
:user.property/single recycled-value
|
||||
:user.property/scalar "ok"})]
|
||||
(is (= #{active-value}
|
||||
(:user.property/node properties)))
|
||||
(is (nil? (:user.property/single properties)))
|
||||
(is (= "ok" (:user.property/scalar properties)))
|
||||
(is (= #{:user.property/single}
|
||||
recycled-only-property-ids))))
|
||||
|
||||
(deftest sanitize-property-values-for-display-marks-all-recycled-coll-as-hidden-test
|
||||
(let [recycled-a {:db/id 201
|
||||
:block/title "Recycled A"
|
||||
:logseq.property/deleted-at 1}
|
||||
recycled-b {:db/id 202
|
||||
:block/title "Recycled B"
|
||||
:logseq.property/deleted-at 2}
|
||||
{:keys [properties recycled-only-property-ids]}
|
||||
(#'property-component/sanitize-property-values-for-display
|
||||
{:user.property/nodes [recycled-a recycled-b]})]
|
||||
(is (nil? (:user.property/nodes properties)))
|
||||
(is (= #{:user.property/nodes}
|
||||
recycled-only-property-ids))))
|
||||
67
src/test/frontend/components/property/value_test.cljs
Normal file
67
src/test/frontend/components/property/value_test.cljs
Normal file
@@ -0,0 +1,67 @@
|
||||
(ns frontend.components.property.value-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
[frontend.components.property.value :as property-value]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(deftest resolve-journal-page-for-date-returns-existing-page-test
|
||||
(async done
|
||||
(let [existing-page {:db/id 100
|
||||
:block/journal-day 20250102}
|
||||
created?* (atom false)]
|
||||
(-> (#'property-value/<resolve-journal-page-for-date
|
||||
(js/Date. "2025-01-02T00:00:00Z")
|
||||
(constantly "test-repo")
|
||||
(fn [_repo _title _opts]
|
||||
(p/resolved existing-page))
|
||||
(fn [_title _opts]
|
||||
(reset! created?* true)
|
||||
(p/resolved {:db/id 999
|
||||
:block/journal-day 20250102}))
|
||||
(constantly "Jan 2nd, 2025"))
|
||||
(p/then (fn [page]
|
||||
(is (= existing-page page))
|
||||
(is (false? @created?*))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest resolve-journal-page-for-date-creates-page-when-missing-test
|
||||
(async done
|
||||
(let [created-page {:db/id 200
|
||||
:block/journal-day 20250102}
|
||||
created-calls* (atom [])]
|
||||
(-> (#'property-value/<resolve-journal-page-for-date
|
||||
(js/Date. "2025-01-02T00:00:00Z")
|
||||
(constantly "test-repo")
|
||||
(fn [_repo _title _opts]
|
||||
(p/resolved nil))
|
||||
(fn [title opts]
|
||||
(swap! created-calls* conj [title opts])
|
||||
(p/resolved created-page))
|
||||
(constantly "Jan 2nd, 2025"))
|
||||
(p/then (fn [page]
|
||||
(is (= created-page page))
|
||||
(is (= [["Jan 2nd, 2025" {:redirect? false}]] @created-calls*))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest resolved-property-value-for-render-skips-default-for-placeholder-row-test
|
||||
(let [property {:db/ident :logseq.property/status
|
||||
:logseq.property/default-value {:db/id 3
|
||||
:db/ident :logseq.property/status.todo
|
||||
:block/title "Todo"}}
|
||||
placeholder-block {:db/id 1}]
|
||||
(is (nil? (#'property-value/resolved-property-value-for-render placeholder-block property false)))))
|
||||
|
||||
(deftest resolved-property-value-for-render-uses-default-for-loaded-block-test
|
||||
(let [property {:db/ident :logseq.property/status
|
||||
:logseq.property/default-value {:db/id 3
|
||||
:db/ident :logseq.property/status.todo
|
||||
:block/title "Todo"}}
|
||||
loaded-block {:db/id 1
|
||||
:block/uuid #uuid "11111111-1111-1111-1111-111111111111"}]
|
||||
(is (= (:logseq.property/default-value property)
|
||||
(#'property-value/resolved-property-value-for-render loaded-block property false)))))
|
||||
67
src/test/frontend/config_test.cljs
Normal file
67
src/test/frontend/config_test.cljs
Normal file
@@ -0,0 +1,67 @@
|
||||
(ns frontend.config-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[frontend.config :as config]))
|
||||
|
||||
(deftest custom-url->ws-url-test
|
||||
(testing "https URL becomes wss"
|
||||
(is (= "wss://my-server.example.com/sync/%s"
|
||||
(config/custom-url->ws-url "https://my-server.example.com"))))
|
||||
|
||||
(testing "http URL becomes ws"
|
||||
(is (= "ws://localhost:8787/sync/%s"
|
||||
(config/custom-url->ws-url "http://localhost:8787"))))
|
||||
|
||||
(testing "trailing slashes are stripped"
|
||||
(is (= "wss://my-server.example.com/sync/%s"
|
||||
(config/custom-url->ws-url "https://my-server.example.com/")))
|
||||
(is (= "wss://my-server.example.com/sync/%s"
|
||||
(config/custom-url->ws-url "https://my-server.example.com///"))))
|
||||
|
||||
(testing "preserves port in URL"
|
||||
(is (= "wss://example.com:3000/sync/%s"
|
||||
(config/custom-url->ws-url "https://example.com:3000"))))
|
||||
|
||||
(testing "preserves subpath in host"
|
||||
;; Users should only provide a base URL, but verify trailing path doesn't break things
|
||||
(is (= "wss://example.com/api/sync/%s"
|
||||
(config/custom-url->ws-url "https://example.com/api")))))
|
||||
|
||||
(deftest custom-url->http-base-test
|
||||
(testing "returns URL as-is when no trailing slash"
|
||||
(is (= "https://my-server.example.com"
|
||||
(config/custom-url->http-base "https://my-server.example.com"))))
|
||||
|
||||
(testing "strips trailing slashes"
|
||||
(is (= "https://my-server.example.com"
|
||||
(config/custom-url->http-base "https://my-server.example.com/")))
|
||||
(is (= "https://my-server.example.com"
|
||||
(config/custom-url->http-base "https://my-server.example.com///"))))
|
||||
|
||||
(testing "preserves http scheme"
|
||||
(is (= "http://localhost:8787"
|
||||
(config/custom-url->http-base "http://localhost:8787"))))
|
||||
|
||||
(testing "preserves port"
|
||||
(is (= "https://example.com:3000"
|
||||
(config/custom-url->http-base "https://example.com:3000/")))))
|
||||
|
||||
(deftest default-urls-are-returned-when-no-custom-url
|
||||
(testing "db-sync-ws-url returns default when no custom URL is set"
|
||||
;; In test environment, node-test? is true so get-custom-sync-server-url
|
||||
;; always returns nil, meaning we always get the default
|
||||
(is (string? (config/db-sync-ws-url)))
|
||||
(is (= config/default-db-sync-ws-url (config/db-sync-ws-url))))
|
||||
|
||||
(testing "db-sync-http-base returns default when no custom URL is set"
|
||||
(is (string? (config/db-sync-http-base)))
|
||||
(is (= config/default-db-sync-http-base (config/db-sync-http-base)))))
|
||||
|
||||
(deftest valid-sync-server-url?-test
|
||||
(testing "accepts http and https URLs"
|
||||
(is (config/valid-sync-server-url? "https://my-server.example.com"))
|
||||
(is (config/valid-sync-server-url? "http://localhost:8787")))
|
||||
|
||||
(testing "rejects non-URL strings"
|
||||
(is (not (config/valid-sync-server-url? "not a url")))
|
||||
(is (not (config/valid-sync-server-url? "")))
|
||||
(is (not (config/valid-sync-server-url? nil)))))
|
||||
@@ -86,6 +86,20 @@
|
||||
(is (= 1 (:db/id (db/entity test-db 1))))
|
||||
(is (= 1 (:db/id (db/entity (conn/get-db test-db) 1)))))
|
||||
|
||||
(deftest get-latest-journals-should-exclude-recycled-journal-pages
|
||||
(load-test-files
|
||||
[{:page {:build/journal 20240101}}
|
||||
{:page {:build/journal 20240102}}
|
||||
{:page {:build/journal 20240103}}])
|
||||
(let [journal-2024-01-02-id (ffirst (d/q '[:find ?e
|
||||
:where
|
||||
[?e :block/journal-day 20240102]]
|
||||
(conn/get-db test-db)))]
|
||||
(db/transact! test-db [{:db/id journal-2024-01-02-id
|
||||
:logseq.property/deleted-at 1704196800000}]))
|
||||
(is (= [20240103 20240101]
|
||||
(mapv :block/journal-day (model/get-latest-journals test-db 10)))))
|
||||
|
||||
(deftest get-block-by-page-name-and-block-route-name
|
||||
(load-test-files
|
||||
[{:page {:block/title "foo"}
|
||||
|
||||
46
src/test/frontend/db/property_values_test.cljs
Normal file
46
src/test/frontend/db/property_values_test.cljs
Normal file
@@ -0,0 +1,46 @@
|
||||
(ns frontend.db.property-values-test
|
||||
(:require [cljs.test :refer [deftest is use-fixtures]]
|
||||
[datascript.core :as d]
|
||||
[frontend.db :as db]
|
||||
[frontend.test.helper :as test-helper]
|
||||
[logseq.db.common.entity-plus :as entity-plus]
|
||||
[logseq.db.common.view :as db-view]))
|
||||
|
||||
(def repo test-helper/test-db)
|
||||
|
||||
(defn start-and-destroy-db
|
||||
[f]
|
||||
(test-helper/start-and-destroy-db f))
|
||||
|
||||
(use-fixtures :each start-and-destroy-db)
|
||||
|
||||
(deftest get-property-values-filters-recycled-ref-values-test
|
||||
(let [property-ident :block/tags
|
||||
active-title "Active ref value"
|
||||
recycled-title "Recycled ref value"]
|
||||
(d/transact! (db/get-db repo false)
|
||||
[[:db/add -2 :block/title active-title]
|
||||
[:db/add -3 :block/title recycled-title]
|
||||
[:db/add -3 :logseq.property/deleted-at 1]
|
||||
[:db/add -10 property-ident -2]
|
||||
[:db/add -11 property-ident -3]])
|
||||
(let [result (db-view/get-property-values @(db/get-db repo false) property-ident {})]
|
||||
(is (contains? (set (map :label result)) active-title))
|
||||
(is (not (contains? (set (map :label result)) recycled-title))))))
|
||||
|
||||
(deftest property-closed-values-hide-recycled-values-test
|
||||
(d/transact! (db/get-db repo false)
|
||||
[{:db/id -1 :db/ident :user.property/closed-values-visibility}
|
||||
{:db/id -2
|
||||
:block/title "Visible closed value"
|
||||
:block/order "a"
|
||||
:block/closed-value-property -1}
|
||||
{:db/id -3
|
||||
:block/title "Recycled closed value"
|
||||
:block/order "b"
|
||||
:block/closed-value-property -1
|
||||
:logseq.property/deleted-at 1}])
|
||||
(let [db @(db/get-db repo false)
|
||||
property (d/entity db :user.property/closed-values-visibility)
|
||||
values (entity-plus/lookup-kv-then-entity property :property/closed-values)]
|
||||
(is (= ["Visible closed value"] (map :block/title values)))))
|
||||
36
src/test/frontend/handler/common/page_test.cljs
Normal file
36
src/test/frontend/handler/common/page_test.cljs
Normal file
@@ -0,0 +1,36 @@
|
||||
(ns frontend.handler.common.page-test
|
||||
(:require [clojure.test :refer [async is use-fixtures]]
|
||||
[datascript.core :as d]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.common.page :as page-common-handler]
|
||||
[frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.page :as outliner-page]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(use-fixtures :each
|
||||
{:before (fn []
|
||||
(async done
|
||||
(test-helper/start-test-db!)
|
||||
(done)))
|
||||
:after test-helper/destroy-test-db!})
|
||||
|
||||
(deftest-async create-page-restores-recycled-page
|
||||
(test-helper/load-test-files [{:page {:block/title "foo"}
|
||||
:blocks [{:block/title "child block"}]}])
|
||||
(p/let [conn (db/get-db test-helper/test-db false)
|
||||
page (db-test/find-page-by-title @conn "foo")
|
||||
page-uuid (:block/uuid page)
|
||||
_ (outliner-page/delete! conn page-uuid {})
|
||||
recycled-page (d/entity @conn [:block/uuid page-uuid])
|
||||
_ (is (ldb/recycled? recycled-page)
|
||||
"Page should be recycled after deletion")
|
||||
restored-page (page-common-handler/<create! "foo" {:redirect? false})]
|
||||
(is (= (:db/id restored-page) (:db/id page))
|
||||
"create! should return the restored page")
|
||||
(let [page' (d/entity @conn [:block/uuid page-uuid])]
|
||||
(is (not (ldb/recycled? page'))
|
||||
"Page should no longer be recycled after re-creation")
|
||||
(is (= "foo" (get-in (db-test/find-block-by-content @conn "child block") [:block/page :block/title]))
|
||||
"Restored page still has its block(s)"))))
|
||||
@@ -7,35 +7,8 @@
|
||||
[frontend.handler.user :as user-handler]
|
||||
[frontend.state :as state]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db-sync.snapshot :as snapshot]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn- encode-datoms-jsonl [datoms]
|
||||
(snapshot/encode-datoms-jsonl datoms))
|
||||
|
||||
(defn- <gzip-bytes [^js payload]
|
||||
(if (exists? js/CompressionStream)
|
||||
(p/let [stream (js/ReadableStream.
|
||||
#js {:start (fn [controller]
|
||||
(.enqueue controller payload)
|
||||
(.close controller))})
|
||||
compressed (.pipeThrough stream (js/CompressionStream. "gzip"))
|
||||
resp (js/Response. compressed)
|
||||
buf (.arrayBuffer resp)]
|
||||
(js/Uint8Array. buf))
|
||||
(p/resolved payload)))
|
||||
|
||||
(defn- bytes->stream
|
||||
[^js payload chunk-size]
|
||||
(js/ReadableStream.
|
||||
#js {:start (fn [controller]
|
||||
(loop [offset 0]
|
||||
(when (< offset (.-byteLength payload))
|
||||
(.enqueue controller (.slice payload offset (min (+ offset chunk-size)
|
||||
(.-byteLength payload))))
|
||||
(recur (+ offset chunk-size))))
|
||||
(.close controller))}))
|
||||
|
||||
(deftest remove-member-request-test
|
||||
(async done
|
||||
(let [called (atom nil)]
|
||||
@@ -87,12 +60,18 @@
|
||||
(deftest rtc-create-graph-persists-disabled-e2ee-flag-test
|
||||
(async done
|
||||
(let [fetch-called (atom nil)
|
||||
tx-called (atom nil)]
|
||||
tx-called (atom nil)
|
||||
ensure-calls (atom [])]
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
db/get-db (fn [] :db)
|
||||
ldb/get-graph-schema-version (fn [_] {:major 65})
|
||||
state/<invoke-db-worker (fn [& args]
|
||||
(when (= :thread-api/db-sync-ensure-user-rsa-keys
|
||||
(first args))
|
||||
(swap! ensure-calls conj args))
|
||||
(p/resolved {:public-key "pk"}))
|
||||
db-sync/fetch-json (fn [url opts _]
|
||||
(reset! fetch-called {:url url :opts opts})
|
||||
(p/resolved {:graph-id "graph-1"
|
||||
@@ -110,6 +89,9 @@
|
||||
(is (= "graph-1" graph-id))
|
||||
(is (= "http://base/graphs" (:url @fetch-called)))
|
||||
(is (= false (:graph-e2ee? request-body)))
|
||||
(is (= [[:thread-api/db-sync-ensure-user-rsa-keys
|
||||
{:ensure-server? true}]]
|
||||
@ensure-calls))
|
||||
(is (= :logseq.kv/graph-rtc-e2ee?
|
||||
(get-in tx-data [2 :db/ident])))
|
||||
(is (= false
|
||||
@@ -122,12 +104,18 @@
|
||||
(deftest rtc-create-graph-defaults-e2ee-enabled-test
|
||||
(async done
|
||||
(let [fetch-called (atom nil)
|
||||
tx-called (atom nil)]
|
||||
tx-called (atom nil)
|
||||
ensure-calls (atom [])]
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
db/get-db (fn [] :db)
|
||||
ldb/get-graph-schema-version (fn [_] {:major 65})
|
||||
state/<invoke-db-worker (fn [& args]
|
||||
(when (= :thread-api/db-sync-ensure-user-rsa-keys
|
||||
(first args))
|
||||
(swap! ensure-calls conj args))
|
||||
(p/resolved {:public-key "pk"}))
|
||||
db-sync/fetch-json (fn [url opts _]
|
||||
(reset! fetch-called {:url url :opts opts})
|
||||
(p/resolved {:graph-id "graph-2"}))
|
||||
@@ -145,6 +133,9 @@
|
||||
(is (= "http://base/graphs" (:url @fetch-called)))
|
||||
(is (= true (:graph-e2ee? request-body)))
|
||||
(is (= true (:graph-ready-for-use? request-body)))
|
||||
(is (= [[:thread-api/db-sync-ensure-user-rsa-keys
|
||||
{:ensure-server? true}]]
|
||||
@ensure-calls))
|
||||
(is (= :logseq.kv/graph-rtc-e2ee?
|
||||
(get-in tx-data [2 :db/ident])))
|
||||
(is (= true
|
||||
@@ -189,7 +180,9 @@
|
||||
js/JSON.parse
|
||||
(js->clj :keywordize-keys true))]
|
||||
(is (= false (:graph-ready-for-use? request-body)))
|
||||
(is (= [[:thread-api/db-sync-upload-graph "logseq_db_demo"]]
|
||||
(is (= [[:thread-api/db-sync-ensure-user-rsa-keys
|
||||
{:ensure-server? true}]
|
||||
[:thread-api/db-sync-upload-graph "logseq_db_demo"]]
|
||||
@upload-calls))
|
||||
(is (= 1 @refresh-calls))
|
||||
(is (= ["logseq_db_demo"] @start-calls))
|
||||
@@ -309,7 +302,10 @@
|
||||
|
||||
(deftest get-remote-graphs-includes-ready-for-use-flag-test
|
||||
(async done
|
||||
(let [graphs-state (atom nil)]
|
||||
(let [graphs-state (atom nil)
|
||||
worker-prev @state/*db-worker
|
||||
ensure-calls (atom [])]
|
||||
(reset! state/*db-worker :worker)
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
@@ -320,7 +316,13 @@
|
||||
:graph-e2ee? true
|
||||
:graph-ready-for-use? false
|
||||
:created-at 1
|
||||
:updated-at 2}]}))
|
||||
:updated-at 2}]
|
||||
:user-rsa-keys-exists? true}))
|
||||
state/<invoke-db-worker (fn [& args]
|
||||
(when (= :thread-api/db-sync-ensure-user-rsa-keys
|
||||
(first args))
|
||||
(swap! ensure-calls conj args))
|
||||
(p/resolved :ok))
|
||||
state/set-state! (fn [k v]
|
||||
(when (= k :rtc/graphs)
|
||||
(reset! graphs-state v))
|
||||
@@ -330,159 +332,90 @@
|
||||
(p/then (fn [graphs]
|
||||
(is (= false (:graph-ready-for-use? (first graphs))))
|
||||
(is (= false (:graph-ready-for-use? (first @graphs-state))))
|
||||
(is (empty? @ensure-calls))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest rtc-download-graph-imports-snapshot-once-test
|
||||
(async done
|
||||
(let [import-calls (atom [])
|
||||
fetch-calls (atom [])
|
||||
datoms [{:e 1 :a :db/ident :v :logseq.class/Page :tx 1 :added true}
|
||||
{:e 2 :a :block/title :v "hello" :tx 1 :added true}]
|
||||
jsonl-bytes (encode-datoms-jsonl datoms)
|
||||
original-fetch js/fetch
|
||||
download-url "http://base/sync/graph-1/snapshot/download"
|
||||
asset-url "http://base/assets/graph-1/snapshot-1.snapshot"]
|
||||
(-> (p/let [gzip-bytes (<gzip-bytes jsonl-bytes)]
|
||||
(set! js/fetch
|
||||
(fn [url opts]
|
||||
(let [method (or (aget opts "method") "GET")]
|
||||
(swap! fetch-calls conj [url method])
|
||||
(cond
|
||||
(and (= url asset-url) (= method "GET"))
|
||||
(js/Promise.resolve
|
||||
#js {:ok true
|
||||
:status 200
|
||||
:headers #js {:get (fn [header]
|
||||
(when (= header "content-length")
|
||||
(str (.-byteLength gzip-bytes))))}
|
||||
:arrayBuffer (fn [] (js/Promise.resolve (.-buffer gzip-bytes)))})
|
||||
|
||||
:else
|
||||
(js/Promise.resolve
|
||||
#js {:ok true
|
||||
:status 200})))))
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
db-sync/fetch-json (fn [url _opts _schema]
|
||||
(cond
|
||||
(string/ends-with? url "/pull")
|
||||
(p/resolved {:t 42})
|
||||
|
||||
(= url download-url)
|
||||
(p/resolved {:ok true
|
||||
:url asset-url
|
||||
:key "graph-1/snapshot-1.snapshot"
|
||||
:content-encoding "gzip"})
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "unexpected fetch-json URL"
|
||||
{:url url}))))
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
state/<invoke-db-worker (fn [& args]
|
||||
(swap! import-calls conj args)
|
||||
(if (= :thread-api/db-sync-import-prepare (first args))
|
||||
(p/resolved {:import-id "import-1"})
|
||||
(p/resolved :ok)))
|
||||
state/set-state! (fn [& _] nil)
|
||||
state/pub-event! (fn [& _] nil)]
|
||||
(db-sync/<rtc-download-graph! "demo-graph" "graph-1" false))
|
||||
(p/finally (fn [] (set! js/fetch original-fetch)))))
|
||||
(p/then (fn [_]
|
||||
(is (= 3 (count @import-calls)))
|
||||
(let [[prepare-op graph reset? graph-uuid graph-e2ee?] (first @import-calls)
|
||||
[chunk-op imported-datoms chunk-graph-uuid import-id] (second @import-calls)
|
||||
[finalize-op finalize-graph finalize-graph-uuid remote-tx finalize-import-id] (nth @import-calls 2)]
|
||||
(is (= :thread-api/db-sync-import-prepare prepare-op))
|
||||
(is (string/ends-with? graph "demo-graph"))
|
||||
(is (= true reset?))
|
||||
(is (= "graph-1" graph-uuid))
|
||||
(is (= false graph-e2ee?))
|
||||
(is (= :thread-api/db-sync-import-datoms-chunk chunk-op))
|
||||
(is (= datoms imported-datoms))
|
||||
(is (= "graph-1" chunk-graph-uuid))
|
||||
(is (= "import-1" import-id))
|
||||
(is (= :thread-api/db-sync-import-finalize finalize-op))
|
||||
(is (string/ends-with? finalize-graph "demo-graph"))
|
||||
(is (= "graph-1" finalize-graph-uuid))
|
||||
(is (= 42 remote-tx))
|
||||
(is (= "import-1" finalize-import-id)))
|
||||
(is (= [[asset-url "GET"]]
|
||||
@fetch-calls))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(set! js/fetch original-fetch)
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest rtc-download-graph-streams-gzip-snapshot-test
|
||||
(async done
|
||||
(let [import-calls (atom [])
|
||||
datoms [{:e 1 :a :db/ident :v :logseq.class/Page :tx 1 :added true}
|
||||
{:e 2 :a :block/title :v "hello" :tx 1 :added true}]
|
||||
jsonl-bytes (encode-datoms-jsonl datoms)
|
||||
original-fetch js/fetch
|
||||
download-url "http://base/sync/graph-1/snapshot/download"
|
||||
asset-url "http://base/assets/graph-1/snapshot-1.snapshot"
|
||||
worker-prev @state/*db-worker]
|
||||
(reset! state/*db-worker nil)
|
||||
(-> (p/let [gzip-bytes (<gzip-bytes jsonl-bytes)
|
||||
stream (bytes->stream gzip-bytes 3)]
|
||||
(set! js/fetch
|
||||
(fn [url opts]
|
||||
(let [method (or (aget opts "method") "GET")]
|
||||
(cond
|
||||
(and (= url asset-url) (= method "GET"))
|
||||
(js/Promise.resolve
|
||||
#js {:ok true
|
||||
:status 200
|
||||
:headers #js {:get (fn [header]
|
||||
(case header
|
||||
"content-length" (str (.-byteLength gzip-bytes))
|
||||
"content-encoding" "gzip"
|
||||
nil))}
|
||||
:body stream
|
||||
:arrayBuffer (fn [] (throw (js/Error. "arrayBuffer should not be used")))})
|
||||
:else
|
||||
(js/Promise.resolve #js {:ok false :status 404})))))
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
db-sync/fetch-json (fn [url _opts _schema]
|
||||
(cond
|
||||
(string/ends-with? url "/pull")
|
||||
(p/resolved {:t 42})
|
||||
|
||||
(= url download-url)
|
||||
(p/resolved {:ok true
|
||||
:url asset-url
|
||||
:key "graph-1/snapshot-1.snapshot"
|
||||
:content-encoding "gzip"})
|
||||
|
||||
:else
|
||||
(p/rejected (ex-info "unexpected fetch-json URL"
|
||||
{:url url}))))
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
state/<invoke-db-worker (fn [& args]
|
||||
(swap! import-calls conj args)
|
||||
(if (= :thread-api/db-sync-import-prepare (first args))
|
||||
(p/resolved {:import-id "import-1"})
|
||||
(p/resolved :ok)))
|
||||
state/set-state! (fn [& _] nil)
|
||||
state/pub-event! (fn [& _] nil)]
|
||||
(db-sync/<rtc-download-graph! "demo-graph" "graph-1" false))
|
||||
(p/finally (fn [] (set! js/fetch original-fetch)))))
|
||||
(p/then (fn [_]
|
||||
(is (= 3 (count @import-calls)))
|
||||
(let [[chunk-op imported-datoms _ import-id] (second @import-calls)]
|
||||
(is (= :thread-api/db-sync-import-datoms-chunk chunk-op))
|
||||
(is (= datoms imported-datoms))
|
||||
(is (= "import-1" import-id)))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(reset! state/*db-worker worker-prev)
|
||||
(set! js/fetch original-fetch)
|
||||
(is false (str error))
|
||||
(done)))
|
||||
(p/finally (fn []
|
||||
(reset! state/*db-worker worker-prev)))))))
|
||||
|
||||
(deftest get-remote-graphs-ensures-user-rsa-keys-when-server-missing-test
|
||||
(async done
|
||||
(let [worker-prev @state/*db-worker
|
||||
ensure-calls (atom [])]
|
||||
(reset! state/*db-worker :worker)
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
db-sync/fetch-json (fn [_url _opts _schema]
|
||||
(p/resolved {:graphs []
|
||||
:user-rsa-keys-exists? false}))
|
||||
state/<invoke-db-worker (fn [& args]
|
||||
(when (= :thread-api/db-sync-ensure-user-rsa-keys
|
||||
(first args))
|
||||
(swap! ensure-calls conj args))
|
||||
(p/resolved {:public-key "pk"}))
|
||||
state/set-state! (fn [& _] nil)
|
||||
repo-handler/refresh-repos! (fn [] nil)]
|
||||
(db-sync/<get-remote-graphs))
|
||||
(p/then (fn [_]
|
||||
(is (= [[:thread-api/db-sync-ensure-user-rsa-keys
|
||||
{:ensure-server? true
|
||||
:server-rsa-keys-exists? false}]]
|
||||
@ensure-calls))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))
|
||||
(p/finally (fn []
|
||||
(reset! state/*db-worker worker-prev)))))))
|
||||
|
||||
(deftest rtc-download-graph-delegates-to-worker-download-api-test
|
||||
(async done
|
||||
(let [worker-calls (atom [])]
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
state/<invoke-db-worker (fn [& args]
|
||||
(swap! worker-calls conj args)
|
||||
(p/resolved :ok))
|
||||
state/set-state! (fn [& _] nil)
|
||||
state/pub-event! (fn [& _] nil)]
|
||||
(db-sync/<rtc-download-graph! "demo-graph" "graph-1" false))
|
||||
(p/then (fn [_]
|
||||
(is (= 1 (count @worker-calls)))
|
||||
(let [[op graph graph-uuid graph-e2ee?] (first @worker-calls)]
|
||||
(is (= :thread-api/db-sync-download-graph op))
|
||||
(is (string/ends-with? graph "demo-graph"))
|
||||
(is (= "graph-1" graph-uuid))
|
||||
(is (= false graph-e2ee?)))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest rtc-download-graph-sets-and-clears-downloading-state-test
|
||||
(async done
|
||||
(let [state-calls (atom [])
|
||||
worker-prev @state/*db-worker]
|
||||
(reset! state/*db-worker nil)
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
state/<invoke-db-worker (fn [& _] (p/resolved :ok))
|
||||
state/pub-event! (fn [& _] nil)
|
||||
state/set-state! (fn [k v]
|
||||
(swap! state-calls conj [k v])
|
||||
nil)]
|
||||
(db-sync/<rtc-download-graph! "demo-graph" "graph-1" false))
|
||||
(p/then (fn [_]
|
||||
(is (= [[:rtc/downloading-graph-uuid "graph-1"]
|
||||
[:rtc/downloading-graph-uuid nil]]
|
||||
@state-calls))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(reset! state/*db-worker worker-prev)
|
||||
(is false (str error))
|
||||
(done)))
|
||||
(p/finally (fn []
|
||||
|
||||
@@ -74,14 +74,13 @@
|
||||
[(missing? $ ?b :logseq.property/deleted-at)]]
|
||||
@conn)
|
||||
(map (comp :block/title first)))
|
||||
recycled-blocks (->> (d/q '[:find (pull ?b [*])
|
||||
:where
|
||||
[?b :logseq.property/deleted-at]
|
||||
[?b :block/title ""]]
|
||||
@conn)
|
||||
(map first))]
|
||||
deleted-blocks (->> (d/q '[:find (pull ?b [*])
|
||||
:where
|
||||
[?b :block/title ""]]
|
||||
@conn)
|
||||
(map first))]
|
||||
(is (= ["b1" "b2"] updated-blocks) "Visible page blocks stay on the page")
|
||||
(is (= 1 (count recycled-blocks)) "Deleted block moves to recycle")))})))
|
||||
(is (empty? deleted-blocks) "Deleted block is removed from page db")))})))
|
||||
|
||||
(testing "backspace deletes empty block in embedded context"
|
||||
;; testing embed at this layer doesn't require an embed block since
|
||||
@@ -104,11 +103,10 @@
|
||||
[(missing? $ ?b :logseq.property/deleted-at)]]
|
||||
@conn)
|
||||
(map (comp :block/title first)))
|
||||
recycled-blocks (->> (d/q '[:find (pull ?b [*])
|
||||
:where
|
||||
[?b :logseq.property/deleted-at]
|
||||
[?b :block/title ""]]
|
||||
@conn)
|
||||
(map first))]
|
||||
deleted-blocks (->> (d/q '[:find (pull ?b [*])
|
||||
:where
|
||||
[?b :block/title ""]]
|
||||
@conn)
|
||||
(map first))]
|
||||
(is (= ["b1" "b2"] updated-blocks) "Visible page blocks stay on the page")
|
||||
(is (= 1 (count recycled-blocks)) "Deleted block moves to recycle")))}))))
|
||||
(is (empty? deleted-blocks) "Deleted block is removed from page db")))}))))
|
||||
|
||||
107
src/test/frontend/handler/history_test.cljs
Normal file
107
src/test/frontend/handler/history_test.cljs
Normal file
@@ -0,0 +1,107 @@
|
||||
(ns frontend.handler.history-test
|
||||
(:require [clojure.test :refer [deftest is]]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.editor :as editor]
|
||||
[frontend.handler.history :as history]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[logseq.db :as ldb]))
|
||||
|
||||
(deftest restore-cursor-and-state-prefers-ui-state-test
|
||||
(let [pause-calls (atom [])
|
||||
app-state-calls (atom [])
|
||||
cursor-calls (atom [])]
|
||||
(with-redefs [state/set-state! (fn [k v]
|
||||
(swap! pause-calls conj [k v]))
|
||||
ldb/read-transit-str (fn [_]
|
||||
{:old-state {:route-data {:to :page}}
|
||||
:new-state {:route-data {:to :home}}})
|
||||
history/restore-app-state! (fn [app-state]
|
||||
(swap! app-state-calls conj app-state))
|
||||
history/restore-cursor! (fn [data]
|
||||
(swap! cursor-calls conj data))]
|
||||
(#'history/restore-cursor-and-state!
|
||||
{:ui-state-str "ui-state"
|
||||
:undo? true
|
||||
:editor-cursors [{:block-uuid (random-uuid)}]})
|
||||
(is (= [[:history/paused? true]
|
||||
[:history/paused? false]]
|
||||
@pause-calls))
|
||||
(is (= [{:route-data {:to :page}}]
|
||||
@app-state-calls))
|
||||
(is (empty? @cursor-calls)))))
|
||||
|
||||
(deftest restore-cursor-and-state-falls-back-to-cursor-test
|
||||
(let [pause-calls (atom [])
|
||||
app-state-calls (atom [])
|
||||
cursor-calls (atom [])]
|
||||
(with-redefs [state/set-state! (fn [k v]
|
||||
(swap! pause-calls conj [k v]))
|
||||
history/restore-app-state! (fn [app-state]
|
||||
(swap! app-state-calls conj app-state))
|
||||
history/restore-cursor! (fn [data]
|
||||
(swap! cursor-calls conj data))]
|
||||
(#'history/restore-cursor-and-state!
|
||||
{:ui-state-str nil
|
||||
:undo? false
|
||||
:editor-cursors [{:block-uuid (random-uuid)
|
||||
:start-pos 1
|
||||
:end-pos 2}]})
|
||||
(is (= [[:history/paused? true]
|
||||
[:history/paused? false]]
|
||||
@pause-calls))
|
||||
(is (empty? @app-state-calls))
|
||||
(is (= 1 (count @cursor-calls)))
|
||||
(is (nil? (:ui-state-str (first @cursor-calls))))
|
||||
(is (= false (:undo? (first @cursor-calls)))))))
|
||||
|
||||
(deftest restore-cursor-prefers-block-selection-test
|
||||
(let [selection-calls (atom [])
|
||||
edit-calls (atom [])]
|
||||
(with-redefs [util/get-blocks-by-id (fn [block-id]
|
||||
(case block-id
|
||||
#uuid "00000000-0000-0000-0000-000000000001" [:node-1]
|
||||
#uuid "00000000-0000-0000-0000-000000000002" [:node-2]
|
||||
nil))
|
||||
state/exit-editing-and-set-selected-blocks! (fn [blocks direction]
|
||||
(swap! selection-calls conj [blocks direction]))
|
||||
editor/edit-block! (fn [& args]
|
||||
(swap! edit-calls conj args))
|
||||
db/pull (constantly nil)]
|
||||
(#'history/restore-cursor!
|
||||
{:undo? true
|
||||
:editor-cursors [{:selected-block-uuids [#uuid "00000000-0000-0000-0000-000000000001"
|
||||
#uuid "00000000-0000-0000-0000-000000000002"]
|
||||
:selection-direction :down}]})
|
||||
(is (= [[[:node-1 :node-2] :down]]
|
||||
@selection-calls))
|
||||
(is (empty? @edit-calls)))))
|
||||
|
||||
(deftest restore-cursor-selection-falls-back-to-editor-cursor-test
|
||||
(let [selection-calls (atom [])
|
||||
edit-calls (atom [])
|
||||
block-uuid #uuid "00000000-0000-0000-0000-000000000003"]
|
||||
(with-redefs [util/get-blocks-by-id (constantly nil)
|
||||
state/exit-editing-and-set-selected-blocks! (fn [blocks direction]
|
||||
(swap! selection-calls conj [blocks direction]))
|
||||
editor/edit-block! (fn [& args]
|
||||
(swap! edit-calls conj args))
|
||||
db/pull (fn [[_lookup-k id]]
|
||||
(when (= block-uuid id)
|
||||
{:db/id 42
|
||||
:block/uuid block-uuid}))]
|
||||
(#'history/restore-cursor!
|
||||
{:undo? false
|
||||
:editor-cursors [{:selected-block-uuids [#uuid "00000000-0000-0000-0000-000000000001"]
|
||||
:selection-direction :up
|
||||
:block-uuid block-uuid
|
||||
:container-id 99
|
||||
:start-pos 1
|
||||
:end-pos 3}]})
|
||||
(is (empty? @selection-calls))
|
||||
(is (= [[{:db/id 42
|
||||
:block/uuid block-uuid}
|
||||
3
|
||||
{:container-id 99
|
||||
:custom-content nil}]]
|
||||
@edit-calls)))))
|
||||
70
src/test/frontend/handler/user_test.cljs
Normal file
70
src/test/frontend/handler/user_test.cljs
Normal file
@@ -0,0 +1,70 @@
|
||||
(ns frontend.handler.user-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[frontend.handler.user :as user-handler]
|
||||
[frontend.state :as state]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn- with-mocked-local-storage
|
||||
[f]
|
||||
(let [old-storage (.-localStorage js/globalThis)
|
||||
had-local-storage?
|
||||
(.call (.-hasOwnProperty (.-prototype js/Object))
|
||||
js/globalThis
|
||||
"localStorage")
|
||||
mocked-storage #js {:clear (fn [] nil)
|
||||
:setItem (fn [& _] nil)
|
||||
:getItem (fn [& _] nil)
|
||||
:removeItem (fn [& _] nil)}]
|
||||
(js/Object.defineProperty js/globalThis
|
||||
"localStorage"
|
||||
#js {:value mocked-storage
|
||||
:configurable true
|
||||
:writable true})
|
||||
(try
|
||||
(f)
|
||||
(finally
|
||||
(if had-local-storage?
|
||||
(js/Object.defineProperty js/globalThis
|
||||
"localStorage"
|
||||
#js {:value old-storage
|
||||
:configurable true
|
||||
:writable true})
|
||||
(js/Reflect.deleteProperty js/globalThis "localStorage"))))))
|
||||
|
||||
(deftest logout-clears-e2ee-password-when-db-worker-ready-test
|
||||
(testing "logout should request db-worker to clear persisted e2ee password"
|
||||
(let [ops* (atom [])
|
||||
old-worker @state/*db-worker]
|
||||
(reset! state/*db-worker :worker)
|
||||
(try
|
||||
(with-mocked-local-storage
|
||||
(fn []
|
||||
(with-redefs [state/<invoke-db-worker (fn [op & _]
|
||||
(swap! ops* conj op)
|
||||
(p/resolved nil))
|
||||
state/clear-user-info! (fn [] nil)
|
||||
state/pub-event! (fn [& _] nil)
|
||||
user-handler/clear-tokens (fn [] nil)]
|
||||
(user-handler/logout)
|
||||
(is (= [:thread-api/clear-e2ee-password] @ops*)))))
|
||||
(finally
|
||||
(reset! state/*db-worker old-worker))))))
|
||||
|
||||
(deftest logout-skips-e2ee-password-clear-when-db-worker-missing-test
|
||||
(testing "logout should not call db-worker API when db-worker is unavailable"
|
||||
(let [invoke-calls* (atom 0)
|
||||
old-worker @state/*db-worker]
|
||||
(reset! state/*db-worker nil)
|
||||
(try
|
||||
(with-mocked-local-storage
|
||||
(fn []
|
||||
(with-redefs [state/<invoke-db-worker (fn [& _]
|
||||
(swap! invoke-calls* inc)
|
||||
(p/resolved nil))
|
||||
state/clear-user-info! (fn [] nil)
|
||||
state/pub-event! (fn [& _] nil)
|
||||
user-handler/clear-tokens (fn [] nil)]
|
||||
(user-handler/logout)
|
||||
(is (zero? @invoke-calls*)))))
|
||||
(finally
|
||||
(reset! state/*db-worker old-worker))))))
|
||||
@@ -40,7 +40,9 @@
|
||||
#(test-helper/start-and-destroy-db
|
||||
%
|
||||
{:build-init-data? false
|
||||
:schema {:logseq.property/deleted-at {:db/index true}}})
|
||||
:schema {:logseq.property/deleted-at {:db/index true}
|
||||
:logseq.property/created-from-property {:db/index true}
|
||||
}})
|
||||
listen-db-fixture)
|
||||
|
||||
(defn get-block
|
||||
@@ -530,11 +532,7 @@
|
||||
|
||||
(is (= [5] (get-children 4)))
|
||||
|
||||
(let [recycled (get-block 3)
|
||||
recycle-page (db/get-page "Recycle")]
|
||||
(is (some? recycled))
|
||||
(is (integer? (:logseq.property/deleted-at recycled)))
|
||||
(is (= (:db/id recycle-page) (:db/id (:block/page recycled)))))))))
|
||||
(is (nil? (get-block 3)))))))
|
||||
|
||||
(deftest test-bocks-with-level
|
||||
(testing "blocks with level"
|
||||
|
||||
@@ -24,3 +24,18 @@
|
||||
{:shortcuts {:ui/toggle-brackets "t b"}}
|
||||
{:shortcuts {:editor/up ["ctrl+p" "up"]}}))
|
||||
"Map values get merged across configs"))
|
||||
|
||||
(deftest get-editor-info-includes-selection-when-not-editing-test
|
||||
(let [selected-ids [(random-uuid) (random-uuid)]]
|
||||
(with-redefs [state/get-edit-block (constantly nil)
|
||||
state/get-selection-block-ids (constantly selected-ids)
|
||||
state/get-selection-direction (constantly :down)]
|
||||
(is (= {:selected-block-uuids selected-ids
|
||||
:selection-direction :down}
|
||||
(state/get-editor-info))))))
|
||||
|
||||
(deftest get-editor-info-returns-nil-when-not-editing-and-no-selection-test
|
||||
(with-redefs [state/get-edit-block (constantly nil)
|
||||
state/get-selection-block-ids (constantly nil)
|
||||
state/get-selection-direction (constantly nil)]
|
||||
(is (nil? (state/get-editor-info)))))
|
||||
|
||||
@@ -1,672 +1,74 @@
|
||||
(ns frontend.undo-redo-test
|
||||
(:require [clojure.test :as t :refer [deftest is testing use-fixtures]]
|
||||
[datascript.core :as d]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.editor :as editor]
|
||||
[frontend.modules.outliner.core-test :as outliner-test]
|
||||
(:require [clojure.test :refer [deftest is]]
|
||||
[frontend.state :as state]
|
||||
[frontend.test.helper :as test-helper]
|
||||
[frontend.undo-redo :as undo-redo]
|
||||
[frontend.worker.db-listener :as worker-db-listener]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.op :as outliner-op]
|
||||
[logseq.undo-redo-validate :as undo-validate]))
|
||||
[frontend.util :as util]))
|
||||
|
||||
;; TODO: random property ops test
|
||||
;; ADR 0013 note: this namespace keeps main-thread coordination coverage only.
|
||||
;; Worker-owned DB-history recording/replay tests belong under src/test/frontend/worker/.
|
||||
|
||||
(def test-db test-helper/test-db)
|
||||
(deftest undo-redo-proxy-to-worker-test
|
||||
(let [calls (atom [])
|
||||
invoke! (fn [& args]
|
||||
(swap! calls conj (vec args))
|
||||
(vec args))
|
||||
repo "repo-1"]
|
||||
(with-redefs [util/node-test? false
|
||||
state/<invoke-db-worker invoke!]
|
||||
(is (= [:thread-api/undo-redo-undo repo]
|
||||
(undo-redo/undo repo)))
|
||||
(is (= [:thread-api/undo-redo-redo repo]
|
||||
(undo-redo/redo repo)))
|
||||
(is (= [[:thread-api/undo-redo-undo repo]
|
||||
[:thread-api/undo-redo-redo repo]]
|
||||
@calls)))))
|
||||
|
||||
(defmethod worker-db-listener/listen-db-changes :gen-undo-ops
|
||||
[_ {:keys [repo]} tx-report]
|
||||
(undo-redo/gen-undo-ops! repo
|
||||
(-> tx-report
|
||||
(assoc-in [:tx-meta :client-id] (:client-id @state/state))
|
||||
(update-in [:tx-meta :local-tx?] (fn [local-tx?]
|
||||
(if (nil? local-tx?)
|
||||
true
|
||||
local-tx?))))))
|
||||
(deftest clear-history-and-record-editor-info-proxy-test
|
||||
(let [calls (atom [])
|
||||
invoke! (fn [& args]
|
||||
(swap! calls conj (vec args))
|
||||
(vec args))
|
||||
repo "repo-2"
|
||||
editor-info {:block-uuid (random-uuid)
|
||||
:container-id 1
|
||||
:start-pos 0
|
||||
:end-pos 3}]
|
||||
(with-redefs [util/node-test? false
|
||||
state/<invoke-db-worker invoke!]
|
||||
(is (= [:thread-api/undo-redo-clear-history repo]
|
||||
(undo-redo/clear-history! repo)))
|
||||
(is (= [:thread-api/undo-redo-record-editor-info repo editor-info]
|
||||
(undo-redo/record-editor-info! repo editor-info)))
|
||||
(is (= [[:thread-api/undo-redo-clear-history repo]
|
||||
[:thread-api/undo-redo-record-editor-info repo editor-info]]
|
||||
@calls)))))
|
||||
|
||||
(defn listen-db-fixture
|
||||
[f]
|
||||
(let [test-db-conn (db/get-db test-db false)]
|
||||
(assert (some? test-db-conn))
|
||||
(worker-db-listener/listen-db-changes! test-db test-db-conn
|
||||
{:handler-keys [:gen-undo-ops]})
|
||||
(f)
|
||||
(d/unlisten! test-db-conn :frontend.worker.db-listener/listen-db-changes!)))
|
||||
(deftest record-ui-state-proxy-test
|
||||
(let [calls (atom [])
|
||||
invoke! (fn [& args]
|
||||
(swap! calls conj (vec args))
|
||||
(vec args))
|
||||
repo "repo-3"
|
||||
ui-state-str "{:old-state {}, :new-state {:route-data {:to :page}}}"]
|
||||
(with-redefs [util/node-test? false
|
||||
state/<invoke-db-worker invoke!]
|
||||
(is (nil? (undo-redo/record-ui-state! repo nil)))
|
||||
(is (= [:thread-api/undo-redo-record-ui-state repo ui-state-str]
|
||||
(undo-redo/record-ui-state! repo ui-state-str)))
|
||||
(is (= [[:thread-api/undo-redo-record-ui-state repo ui-state-str]]
|
||||
@calls)))))
|
||||
|
||||
(defn disable-browser-fns
|
||||
[f]
|
||||
;; get-selection-blocks has a js/document reference
|
||||
(with-redefs [state/get-selection-blocks (constantly [])]
|
||||
(f)))
|
||||
|
||||
(defn with-worker-undo-validation
|
||||
[f]
|
||||
(let [orig-transact ldb/transact!]
|
||||
(with-redefs [ldb/transact! (fn [repo-or-conn tx-data tx-meta]
|
||||
(if (and (or (:undo? tx-meta) (:redo? tx-meta))
|
||||
(not (undo-validate/valid-undo-redo-tx? repo-or-conn tx-data)))
|
||||
(throw (ex-info "undo/redo tx invalid"
|
||||
{:undo? (:undo? tx-meta)
|
||||
:redo? (:redo? tx-meta)}))
|
||||
(if (satisfies? IDeref repo-or-conn)
|
||||
(d/transact! repo-or-conn tx-data tx-meta)
|
||||
(orig-transact repo-or-conn tx-data tx-meta))))]
|
||||
(f))))
|
||||
|
||||
(use-fixtures :each
|
||||
disable-browser-fns
|
||||
with-worker-undo-validation
|
||||
test-helper/react-components
|
||||
#(test-helper/start-and-destroy-db % {:build-init-data? false
|
||||
:schema {:logseq.property/deleted-at {:db/index true}}})
|
||||
listen-db-fixture)
|
||||
|
||||
(defn- undo-all!
|
||||
[]
|
||||
(loop [i 0]
|
||||
(let [r (undo-redo/undo test-db)]
|
||||
(if (not= :frontend.undo-redo/empty-undo-stack r)
|
||||
(recur (inc i))
|
||||
(prn :undo-count i)))))
|
||||
|
||||
(defn- redo-all!
|
||||
[]
|
||||
(loop [i 0]
|
||||
(let [r (undo-redo/redo test-db)]
|
||||
(if (not= :frontend.undo-redo/empty-redo-stack r)
|
||||
(recur (inc i))
|
||||
(prn :redo-count i)))))
|
||||
|
||||
(defn- parent-cycle?
|
||||
[ent]
|
||||
(let [start (:block/uuid ent)]
|
||||
(loop [current ent
|
||||
seen #{start}
|
||||
steps 0]
|
||||
(cond
|
||||
(>= steps 200) true
|
||||
(nil? (:block/parent current)) false
|
||||
:else (let [next-ent (:block/parent current)
|
||||
next-uuid (:block/uuid next-ent)]
|
||||
(if (contains? seen next-uuid)
|
||||
true
|
||||
(recur next-ent (conj seen next-uuid) (inc steps))))))))
|
||||
|
||||
(defn- db-issues
|
||||
[db]
|
||||
(let [ignore-ent? (fn [ent]
|
||||
(or (ldb/recycled? ent)
|
||||
(= "Recycle" (:block/title ent))
|
||||
(= "Recycle" (some-> ent :block/page :block/title))))
|
||||
ents (->> (d/q '[:find [?e ...]
|
||||
:where
|
||||
[?e :block/uuid]]
|
||||
db)
|
||||
(map (fn [e] (d/entity db e)))
|
||||
(remove ignore-ent?))
|
||||
uuid-required-ids (->> (concat
|
||||
(d/q '[:find [?e ...]
|
||||
:where
|
||||
[?e :block/title]]
|
||||
db)
|
||||
(d/q '[:find [?e ...]
|
||||
:where
|
||||
[?e :block/page]]
|
||||
db)
|
||||
(d/q '[:find [?e ...]
|
||||
:where
|
||||
[?e :block/parent]]
|
||||
db))
|
||||
distinct)]
|
||||
(concat
|
||||
(for [e uuid-required-ids
|
||||
:let [ent (d/entity db e)]
|
||||
:when (and (not (ignore-ent? ent))
|
||||
(nil? (:block/uuid ent)))]
|
||||
{:type :missing-uuid :e e})
|
||||
(for [ent ents
|
||||
:let [uuid (:block/uuid ent)
|
||||
parent (:block/parent ent)]
|
||||
:when (and (not (ldb/page? ent)) (nil? parent))]
|
||||
{:type :missing-parent :uuid uuid})
|
||||
(for [ent ents
|
||||
:let [uuid (:block/uuid ent)
|
||||
parent (:block/parent ent)]
|
||||
:when (and (not (ldb/page? ent)) parent (nil? (:block/uuid parent)))]
|
||||
{:type :missing-parent-ref :uuid uuid})
|
||||
(for [ent ents
|
||||
:let [uuid (:block/uuid ent)
|
||||
page (:block/page ent)]
|
||||
:when (and (not (ldb/page? ent)) (nil? page))]
|
||||
{:type :missing-page :uuid uuid})
|
||||
(for [ent ents
|
||||
:let [uuid (:block/uuid ent)
|
||||
page (:block/page ent)]
|
||||
:when (and (not (ldb/page? ent)) page (not (ldb/page? page)))]
|
||||
{:type :page-not-page :uuid uuid})
|
||||
(for [ent ents
|
||||
:let [uuid (:block/uuid ent)
|
||||
parent (:block/parent ent)
|
||||
page (:block/page ent)
|
||||
expected-page (when parent
|
||||
(if (ldb/page? parent) parent (:block/page parent)))]
|
||||
:when (and (not (ldb/page? ent))
|
||||
parent
|
||||
page
|
||||
expected-page
|
||||
(not= (:block/uuid expected-page) (:block/uuid page)))]
|
||||
{:type :page-mismatch :uuid uuid})
|
||||
(for [ent ents
|
||||
:let [uuid (:block/uuid ent)
|
||||
parent (:block/parent ent)]
|
||||
:when (and parent (= uuid (:block/uuid parent)))]
|
||||
{:type :self-parent :uuid uuid})
|
||||
(for [ent ents
|
||||
:let [uuid (:block/uuid ent)]
|
||||
:when (and (not (ldb/page? ent))
|
||||
(parent-cycle? ent))]
|
||||
{:type :cycle :uuid uuid}))))
|
||||
|
||||
(defn- seed-page-parent-child!
|
||||
[]
|
||||
(let [conn (db/get-db test-db false)
|
||||
page-uuid (random-uuid)
|
||||
parent-uuid (random-uuid)
|
||||
child-uuid (random-uuid)]
|
||||
(d/transact! conn
|
||||
[{:db/ident :logseq.class/Page}
|
||||
{:block/uuid page-uuid
|
||||
:block/name "page"
|
||||
:block/title "page"
|
||||
:block/tags #{:logseq.class/Page}}
|
||||
{:block/uuid parent-uuid
|
||||
:block/title "parent"
|
||||
:block/page [:block/uuid page-uuid]
|
||||
:block/parent [:block/uuid page-uuid]}
|
||||
{:block/uuid child-uuid
|
||||
:block/title "child"
|
||||
:block/page [:block/uuid page-uuid]
|
||||
:block/parent [:block/uuid parent-uuid]}]
|
||||
{:outliner-op :insert-blocks
|
||||
:local-tx? false})
|
||||
{:page-uuid page-uuid
|
||||
:parent-uuid parent-uuid
|
||||
:child-uuid child-uuid}))
|
||||
|
||||
(defn- seed-page-two-parents-child!
|
||||
[]
|
||||
(let [conn (db/get-db test-db false)
|
||||
page-uuid (random-uuid)
|
||||
parent-a-uuid (random-uuid)
|
||||
parent-b-uuid (random-uuid)
|
||||
child-uuid (random-uuid)]
|
||||
(d/transact! conn
|
||||
[{:db/ident :logseq.class/Page}
|
||||
{:block/uuid page-uuid
|
||||
:block/name "page"
|
||||
:block/title "page"
|
||||
:block/tags #{:logseq.class/Page}}
|
||||
{:block/uuid parent-a-uuid
|
||||
:block/title "parent-a"
|
||||
:block/page [:block/uuid page-uuid]
|
||||
:block/parent [:block/uuid page-uuid]}
|
||||
{:block/uuid parent-b-uuid
|
||||
:block/title "parent-b"
|
||||
:block/page [:block/uuid page-uuid]
|
||||
:block/parent [:block/uuid page-uuid]}
|
||||
{:block/uuid child-uuid
|
||||
:block/title "child"
|
||||
:block/page [:block/uuid page-uuid]
|
||||
:block/parent [:block/uuid parent-a-uuid]}]
|
||||
{:outliner-op :insert-blocks
|
||||
:local-tx? false})
|
||||
{:page-uuid page-uuid
|
||||
:parent-a-uuid parent-a-uuid
|
||||
:parent-b-uuid parent-b-uuid
|
||||
:child-uuid child-uuid}))
|
||||
|
||||
(deftest undo-records-only-local-txs-test
|
||||
(testing "undo history records only local txs"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/title "local-update"]]
|
||||
{:outliner-op :save-block
|
||||
:local-tx? true})
|
||||
(let [undo-result (undo-redo/undo test-db)]
|
||||
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
|
||||
(undo-redo/redo test-db)))
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/title "remote-update"]]
|
||||
{:outliner-op :save-block
|
||||
:local-tx? false})
|
||||
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db))))))
|
||||
|
||||
(deftest single-op-apply-ops-preserves-local-tx-and-client-id-test
|
||||
(testing "single local outliner ops should reach listeners with local/client metadata intact"
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)
|
||||
tx-meta* (atom nil)]
|
||||
(d/listen! conn ::capture-tx-meta
|
||||
(fn [{:keys [tx-meta]}]
|
||||
(reset! tx-meta* tx-meta)))
|
||||
(try
|
||||
(outliner-op/apply-ops! conn
|
||||
[[:save-block [{:block/uuid child-uuid
|
||||
:block/title "single-op-save"} {}]]]
|
||||
{:client-id (:client-id @state/state)
|
||||
:local-tx? true})
|
||||
(is (= true (:local-tx? @tx-meta*)))
|
||||
(is (= (:client-id @state/state) (:client-id @tx-meta*)))
|
||||
(finally
|
||||
(d/unlisten! conn ::capture-tx-meta))))))
|
||||
|
||||
(deftest undo-conflict-clears-history-test
|
||||
(testing "undo clears history when reverse tx is unsafe"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
block-uuid (random-uuid)]
|
||||
(d/transact! conn [{:block/uuid block-uuid
|
||||
:block/title "conflict"}]
|
||||
{:outliner-op :insert-blocks
|
||||
:local-tx? true})
|
||||
(with-redefs [undo-redo/get-reversed-datoms (fn [& _] nil)]
|
||||
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db)))))))
|
||||
|
||||
(deftest undo-works-for-local-graph-test
|
||||
(testing "undo/redo works for local changes on local graph"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/title "local-1"]]
|
||||
{:outliner-op :save-block
|
||||
:local-tx? true})
|
||||
(let [undo-result (undo-redo/undo test-db)]
|
||||
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
|
||||
(is (= "child" (:block/title (d/entity @conn [:block/uuid child-uuid])))))
|
||||
(let [redo-result (undo-redo/redo test-db)]
|
||||
(is (not= :frontend.undo-redo/empty-redo-stack redo-result))
|
||||
(is (= "local-1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))))))
|
||||
|
||||
(deftest undo-insert-retracts-added-entity-cleanly-test
|
||||
(testing "undoing a local insert retracts the inserted entity instead of leaving a partial shell"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [page-uuid]} (seed-page-parent-child!)
|
||||
inserted-uuid (random-uuid)]
|
||||
(d/transact! conn
|
||||
[{:block/uuid inserted-uuid
|
||||
:block/title "inserted"
|
||||
:block/page [:block/uuid page-uuid]
|
||||
:block/parent [:block/uuid page-uuid]}]
|
||||
{:outliner-op :insert-blocks
|
||||
:local-tx? true})
|
||||
(is (some? (d/entity @conn [:block/uuid inserted-uuid])))
|
||||
(let [undo-result (undo-redo/undo test-db)]
|
||||
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
|
||||
(is (nil? (d/entity @conn [:block/uuid inserted-uuid])))))))
|
||||
|
||||
(deftest repeated-save-block-content-undo-redo-test
|
||||
(testing "multiple saves on the same block undo and redo one step at a time"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(doseq [title ["v1" "v2" "v3"]]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/title title]]
|
||||
{:outliner-op :save-block
|
||||
:local-tx? true}))
|
||||
(is (= "v3" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(undo-redo/undo test-db)
|
||||
(is (= "v2" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(undo-redo/undo test-db)
|
||||
(is (= "v1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(undo-redo/undo test-db)
|
||||
(is (= "child" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(undo-redo/redo test-db)
|
||||
(is (= "v1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(undo-redo/redo test-db)
|
||||
(is (= "v2" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(undo-redo/redo test-db)
|
||||
(is (= "v3" (:block/title (d/entity @conn [:block/uuid child-uuid])))))))
|
||||
|
||||
(deftest repeated-editor-save-block-content-undo-redo-test
|
||||
(testing "editor/save-block! records sequential content saves in order"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(doseq [title ["foo" "foo bar"]]
|
||||
(editor/save-block! test-db child-uuid title))
|
||||
(is (= "foo bar" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(undo-redo/undo test-db)
|
||||
(is (= "foo" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(undo-redo/redo test-db)
|
||||
(is (= "foo bar" (:block/title (d/entity @conn [:block/uuid child-uuid])))))))
|
||||
|
||||
(deftest editor-save-two-blocks-undo-targets-latest-block-test
|
||||
(testing "undo after saving two different blocks reverts the latest saved block first"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [parent-uuid child-uuid]} (seed-page-parent-child!)]
|
||||
(editor/save-block! test-db parent-uuid "parent updated")
|
||||
(editor/save-block! test-db child-uuid "child updated")
|
||||
(undo-redo/undo test-db)
|
||||
(is (= "parent updated" (:block/title (d/entity @conn [:block/uuid parent-uuid]))))
|
||||
(is (= "child" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(undo-redo/undo test-db)
|
||||
(is (= "parent" (:block/title (d/entity @conn [:block/uuid parent-uuid])))))))
|
||||
|
||||
(deftest new-local-save-clears-redo-stack-test
|
||||
(testing "a new local save clears redo history"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(editor/save-block! test-db child-uuid "v1")
|
||||
(editor/save-block! test-db child-uuid "v2")
|
||||
(undo-redo/undo test-db)
|
||||
(is (= "v1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
(editor/save-block! test-db child-uuid "v3")
|
||||
(is (= :frontend.undo-redo/empty-redo-stack (undo-redo/redo test-db)))
|
||||
(is (= "v3" (:block/title (d/entity @conn [:block/uuid child-uuid])))))))
|
||||
|
||||
(deftest insert-save-delete-sequence-undo-redo-test
|
||||
(testing "insert then save then recycle-delete can be undone and redone in order"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [page-uuid]} (seed-page-parent-child!)
|
||||
inserted-uuid (random-uuid)
|
||||
recycle-title "Recycle"]
|
||||
(d/transact! conn
|
||||
[{:block/uuid inserted-uuid
|
||||
:block/title "draft"
|
||||
:block/page [:block/uuid page-uuid]
|
||||
:block/parent [:block/uuid page-uuid]}]
|
||||
{:outliner-op :insert-blocks
|
||||
:local-tx? true})
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid inserted-uuid] :block/title "published"]]
|
||||
{:outliner-op :save-block
|
||||
:local-tx? true})
|
||||
(outliner-core/delete-blocks! conn [(d/entity @conn [:block/uuid inserted-uuid])] {})
|
||||
(is (= recycle-title
|
||||
(:block/title (:block/page (d/entity @conn [:block/uuid inserted-uuid])))))
|
||||
(undo-redo/undo test-db)
|
||||
(let [restored (d/entity @conn [:block/uuid inserted-uuid])]
|
||||
(is (= page-uuid (:block/uuid (:block/page restored))))
|
||||
(is (= "published" (:block/title restored))))
|
||||
(undo-redo/undo test-db)
|
||||
(is (= "draft" (:block/title (d/entity @conn [:block/uuid inserted-uuid]))))
|
||||
(undo-redo/undo test-db)
|
||||
(is (nil? (d/entity @conn [:block/uuid inserted-uuid])))
|
||||
(undo-redo/redo test-db)
|
||||
(is (= "draft" (:block/title (d/entity @conn [:block/uuid inserted-uuid]))))
|
||||
(undo-redo/redo test-db)
|
||||
(is (= "published" (:block/title (d/entity @conn [:block/uuid inserted-uuid]))))
|
||||
(undo-redo/redo test-db)
|
||||
(is (= recycle-title
|
||||
(:block/title (:block/page (d/entity @conn [:block/uuid inserted-uuid]))))))))
|
||||
|
||||
(deftest undo-works-with-remote-updates-test
|
||||
(testing "undo works after remote updates on sync graphs"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/title "local-2"]]
|
||||
{:outliner-op :save-block
|
||||
:local-tx? true})
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/updated-at 12345]]
|
||||
{:outliner-op :save-block
|
||||
:local-tx? false})
|
||||
(let [undo-result (undo-redo/undo test-db)]
|
||||
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
|
||||
(is (= "child" (:block/title (d/entity @conn [:block/uuid child-uuid]))))))))
|
||||
|
||||
(deftest undo-redo-works-for-recycle-delete-test
|
||||
(testing "undo restores a recycled delete and redo sends it back to recycle"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid page-uuid]} (seed-page-parent-child!)
|
||||
recycle-page-title "Recycle"]
|
||||
(outliner-core/delete-blocks! conn [(d/entity @conn [:block/uuid child-uuid])] {})
|
||||
(let [deleted-child (d/entity @conn [:block/uuid child-uuid])]
|
||||
(is (integer? (:logseq.property/deleted-at deleted-child)))
|
||||
(is (= recycle-page-title (:block/title (:block/page deleted-child)))))
|
||||
(let [undo-result (undo-redo/undo test-db)
|
||||
restored-child (d/entity @conn [:block/uuid child-uuid])]
|
||||
(is (not= :frontend.undo-redo/empty-undo-stack undo-result))
|
||||
(is (= page-uuid (:block/uuid (:block/page restored-child))))
|
||||
(is (nil? (:logseq.property/deleted-at restored-child))))
|
||||
(let [redo-result (undo-redo/redo test-db)
|
||||
recycled-child (d/entity @conn [:block/uuid child-uuid])]
|
||||
(is (not= :frontend.undo-redo/empty-redo-stack redo-result))
|
||||
(is (= recycle-page-title (:block/title (:block/page recycled-child))))
|
||||
(is (integer? (:logseq.property/deleted-at recycled-child)))))))
|
||||
|
||||
(deftest undo-validation-allows-baseline-issues-test
|
||||
(testing "undo validation allows existing issues without introducing new ones"
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)
|
||||
orphan-uuid (random-uuid)]
|
||||
(d/transact! conn
|
||||
[{:block/uuid orphan-uuid
|
||||
:block/title "orphan"}]
|
||||
{:local-tx? false})
|
||||
(is (undo-validate/valid-undo-redo-tx? conn
|
||||
[[:db/add [:block/uuid child-uuid]
|
||||
:block/title "child-updated"]])))))
|
||||
|
||||
(deftest undo-validation-rejects-invalid-recycle-restore-tx-test
|
||||
(testing "recycle-shaped undo tx still validates resulting structure"
|
||||
(let [conn (db/get-db test-db false)
|
||||
page-uuid (random-uuid)
|
||||
block-uuid (random-uuid)]
|
||||
(d/transact! conn
|
||||
[{:db/ident :logseq.class/Page}
|
||||
{:block/uuid page-uuid
|
||||
:block/name "page"
|
||||
:block/title "page"
|
||||
:block/tags #{:logseq.class/Page}}
|
||||
{:db/id 1000
|
||||
:block/uuid block-uuid
|
||||
:block/title "bad-block"
|
||||
:block/page [:block/uuid page-uuid]
|
||||
:block/parent [:block/uuid page-uuid]
|
||||
:logseq.property/deleted-at 1
|
||||
:logseq.property.recycle/original-page [:block/uuid page-uuid]
|
||||
:logseq.property.recycle/original-parent [:block/uuid page-uuid]
|
||||
:logseq.property.recycle/original-order "aj"}]
|
||||
{:local-tx? false})
|
||||
;; Simulate a broken recycled block shell like the runtime repro: entity has
|
||||
;; structural attrs but no title/uuid dispatch attrs after sync churn.
|
||||
(d/transact! conn
|
||||
[[:db/retract 1000 :block/uuid block-uuid]
|
||||
[:db/retract 1000 :block/title "bad-block"]]
|
||||
{:local-tx? false})
|
||||
(is (false? (undo-validate/valid-undo-redo-tx?
|
||||
conn
|
||||
[[:db/retract 1000 :logseq.property.recycle/original-order "aj"]
|
||||
[:db/retract 1000 :logseq.property/deleted-at 1]
|
||||
[:db/add 1000 :block/parent [:block/uuid page-uuid]]
|
||||
[:db/retract 1000 :logseq.property.recycle/original-page [:block/uuid page-uuid]]
|
||||
[:db/retract 1000 :logseq.property.recycle/original-parent [:block/uuid page-uuid]]
|
||||
[:db/add 1000 :block/order "aj"]
|
||||
[:db/add 1000 :block/page [:block/uuid page-uuid]]]))))))
|
||||
|
||||
(deftest undo-skips-when-parent-missing-test
|
||||
(testing "undo skips when parent is missing"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [parent-uuid child-uuid]} (seed-page-parent-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/retractEntity [:block/uuid child-uuid]]]
|
||||
{:outliner-op :delete-blocks
|
||||
:local-tx? true})
|
||||
(d/transact! conn
|
||||
[[:db/retractEntity [:block/uuid parent-uuid]]]
|
||||
{:outliner-op :delete-blocks
|
||||
:local-tx? false})
|
||||
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db)))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid]))))))
|
||||
|
||||
(deftest undo-skips-when-block-deleted-remote-test
|
||||
(testing "undo skips when block was deleted remotely"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/title "child-updated"]]
|
||||
{:outliner-op :save-block
|
||||
:local-tx? true})
|
||||
(d/transact! conn
|
||||
[[:db/retractEntity [:block/uuid child-uuid]]]
|
||||
{:outliner-op :delete-blocks
|
||||
:local-tx? false})
|
||||
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db)))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid]))))))
|
||||
|
||||
(deftest undo-skips-when-undo-would-create-cycle-test
|
||||
(testing "undo skips when it would create a parent cycle"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [page-uuid parent-uuid child-uuid]} (seed-page-parent-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid page-uuid]]]
|
||||
{:outliner-op :move-blocks
|
||||
:local-tx? true})
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid parent-uuid] :block/parent [:block/uuid child-uuid]]]
|
||||
{:outliner-op :move-blocks
|
||||
:local-tx? false})
|
||||
(is (= :frontend.undo-redo/empty-undo-stack (undo-redo/undo test-db)))
|
||||
(let [parent (d/entity @conn [:block/uuid parent-uuid])
|
||||
child (d/entity @conn [:block/uuid child-uuid])]
|
||||
(is (= child-uuid (:block/uuid (:block/parent parent))))
|
||||
(is (= page-uuid (:block/uuid (:block/parent child))))))))
|
||||
|
||||
(deftest undo-skips-conflicted-move-and-keeps-earlier-history-test
|
||||
(testing "undo skips a conflicting move and continues to earlier safe history"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [parent-a-uuid parent-b-uuid child-uuid]} (seed-page-two-parents-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/title "local-title"]]
|
||||
{:outliner-op :save-block
|
||||
:local-tx? true})
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid parent-b-uuid]]]
|
||||
{:outliner-op :move-blocks
|
||||
:local-tx? true})
|
||||
(d/transact! conn
|
||||
(:tx-data (outliner-core/delete-blocks @conn [(d/entity @conn [:block/uuid parent-a-uuid])] {}))
|
||||
{:outliner-op :delete-blocks
|
||||
:local-tx? false})
|
||||
(let [undo-result (undo-redo/undo test-db)
|
||||
child (d/entity @conn [:block/uuid child-uuid])]
|
||||
(is (map? undo-result))
|
||||
(is (= "child" (:block/title child)))
|
||||
(is (= parent-b-uuid
|
||||
(:block/uuid (:block/parent child))))
|
||||
(is (empty? (db-issues @conn)))))))
|
||||
|
||||
(deftest undo-validation-fast-path-skips-db-issues-for-non-structural-tx-test
|
||||
(testing "undo validation skips db-issues for non-structural tx-data"
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(with-redefs [undo-validate/issues-for-entity-ids (fn [_ _]
|
||||
(throw (js/Error. "issues-for-entity-ids called")))]
|
||||
(is (true? (undo-validate/valid-undo-redo-tx?
|
||||
conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/title "child-updated"]])))))))
|
||||
|
||||
(deftest undo-validation-checks-structural-tx-test
|
||||
(testing "undo validation evaluates structural changes"
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [page-uuid child-uuid]} (seed-page-parent-child!)
|
||||
calls (atom 0)]
|
||||
(with-redefs [undo-validate/issues-for-entity-ids (fn [_ _]
|
||||
(swap! calls inc)
|
||||
[])]
|
||||
(is (true? (undo-validate/valid-undo-redo-tx?
|
||||
conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid page-uuid]]])))
|
||||
(is (pos? @calls))))))
|
||||
|
||||
(deftest redo-builds-reversed-tx-when-target-parent-is-recycled-test
|
||||
(testing "redo still builds reversed tx from raw datoms when target parent was recycled remotely"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid parent-a-uuid parent-b-uuid]} (seed-page-two-parents-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid parent-b-uuid]]]
|
||||
{:outliner-op :move-blocks
|
||||
:local-tx? true})
|
||||
(undo-redo/undo test-db)
|
||||
(d/transact! conn
|
||||
(:tx-data (outliner-core/delete-blocks @conn [(d/entity @conn [:block/uuid parent-b-uuid])] {}))
|
||||
{:outliner-op :delete-blocks
|
||||
:local-tx? false})
|
||||
(let [redo-op (last (get @undo-redo/*redo-ops test-db))
|
||||
data (some #(when (= ::undo-redo/db-transact (first %))
|
||||
(second %))
|
||||
redo-op)
|
||||
reversed (undo-redo/get-reversed-datoms conn false data (:tx-meta data))]
|
||||
(is (seq reversed))
|
||||
(is (= parent-a-uuid
|
||||
(:block/uuid (:block/parent (d/entity @conn [:block/uuid child-uuid])))))))))
|
||||
|
||||
(deftest undo-skips-move-when-original-parent-is-recycled-test
|
||||
(testing "undo should skip a move whose original parent has been recycled"
|
||||
(undo-redo/clear-history! test-db)
|
||||
(let [conn (db/get-db test-db false)
|
||||
{:keys [child-uuid parent-a-uuid parent-b-uuid]} (seed-page-two-parents-child!)]
|
||||
(d/transact! conn
|
||||
[[:db/add [:block/uuid child-uuid] :block/parent [:block/uuid parent-b-uuid]]]
|
||||
{:outliner-op :move-blocks
|
||||
:local-tx? true})
|
||||
(d/transact! conn
|
||||
(:tx-data (outliner-core/delete-blocks @conn [(d/entity @conn [:block/uuid parent-a-uuid])] {}))
|
||||
{:outliner-op :delete-blocks
|
||||
:local-tx? false})
|
||||
(let [parent-a (d/entity @conn [:block/uuid parent-a-uuid])
|
||||
_ (is (some? parent-a))
|
||||
_ (is (true? (ldb/recycled? parent-a)))
|
||||
undo-op (last (get @undo-redo/*undo-ops test-db))
|
||||
data (some #(when (= ::undo-redo/db-transact (first %))
|
||||
(second %))
|
||||
undo-op)
|
||||
conflicted? (#'undo-redo/reversed-structural-target-conflicted?
|
||||
conn
|
||||
(->> (:tx-data data) reverse (group-by :e))
|
||||
true)
|
||||
reversed (undo-redo/get-reversed-datoms conn true data (:tx-meta data))]
|
||||
(is (true? conflicted?))
|
||||
(is (nil? reversed))))))
|
||||
|
||||
(deftest ^:long undo-redo-test
|
||||
(testing "Random mixed operations"
|
||||
(set! undo-redo/max-stack-length 500)
|
||||
(let [*random-blocks (atom (outliner-test/get-blocks-ids))]
|
||||
(outliner-test/transact-random-tree!)
|
||||
(let [conn (db/get-db test-db false)]
|
||||
(d/transact! conn
|
||||
[{:db/ident :logseq.class/Page}
|
||||
[:db/add [:block/uuid 1] :block/tags :logseq.class/Page]]
|
||||
{:local-tx? false}))
|
||||
(let [conn (db/get-db false)
|
||||
_ (outliner-test/run-random-mixed-ops! *random-blocks)]
|
||||
|
||||
(undo-all!)
|
||||
(is (empty? (db-issues @conn)))
|
||||
|
||||
(redo-all!)
|
||||
(is (empty? (db-issues @conn)))))))
|
||||
(deftest node-test-undo-redo-does-not-call-worker-test
|
||||
(let [calls (atom [])
|
||||
invoke! (fn [& args]
|
||||
(swap! calls conj (vec args))
|
||||
(vec args))
|
||||
repo "repo-node"]
|
||||
(with-redefs [util/node-test? true
|
||||
state/<invoke-db-worker invoke!]
|
||||
(is (= :frontend.undo-redo/empty-undo-stack
|
||||
(undo-redo/undo repo)))
|
||||
(is (= :frontend.undo-redo/empty-redo-stack
|
||||
(undo-redo/redo repo)))
|
||||
(is (nil? (undo-redo/clear-history! repo)))
|
||||
(is (empty? @calls)))))
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
(let [next-time (get-next-time one-month-ago month-unit 1)]
|
||||
(is (> (in-days next-time) 1)))
|
||||
(let [next-time (get-next-time one-month-ago month-unit 3)]
|
||||
(is (= 2 (in-months next-time))))
|
||||
(is (contains? #{1 2} (in-months next-time))))
|
||||
(let [next-time (get-next-time one-month-ago month-unit 5)]
|
||||
(is (= 4 (in-months next-time))))
|
||||
(is (contains? #{3 4} (in-months next-time))))
|
||||
|
||||
;; year
|
||||
(let [next-time (get-next-time now year-unit 1)]
|
||||
|
||||
16
src/test/frontend/worker/db_listener_test.cljs
Normal file
16
src/test/frontend/worker/db_listener_test.cljs
Normal file
@@ -0,0 +1,16 @@
|
||||
(ns frontend.worker.db-listener-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[frontend.worker.db-listener :as db-listener]))
|
||||
|
||||
(deftest transit-safe-tx-meta-keeps-outliner-ops-test
|
||||
(testing "worker tx-meta sanitization should preserve semantic outliner ops"
|
||||
(let [outliner-ops [[:save-block [{:block/uuid (random-uuid)
|
||||
:block/title "hello"} nil]]]
|
||||
tx-meta {:outliner-op :save-block
|
||||
:outliner-ops outliner-ops
|
||||
:db-sync/inverse-outliner-ops outliner-ops
|
||||
:error-handler (fn [_] nil)}
|
||||
safe-tx-meta (#'db-listener/transit-safe-tx-meta tx-meta)]
|
||||
(is (= outliner-ops (:outliner-ops safe-tx-meta)))
|
||||
(is (= outliner-ops (:db-sync/inverse-outliner-ops safe-tx-meta)))
|
||||
(is (nil? (:error-handler safe-tx-meta))))))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,21 @@
|
||||
(ns frontend.worker.db-worker-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
[datascript.core :as d]
|
||||
[frontend.common.thread-api :as thread-api]
|
||||
[frontend.worker.a-test-env]
|
||||
[frontend.worker.db-worker :as db-worker]
|
||||
[frontend.worker.search :as search]
|
||||
[frontend.worker.shared-service :as shared-service]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[frontend.worker.sync :as db-sync]
|
||||
[frontend.worker.sync.client-op :as client-op]
|
||||
[frontend.worker.sync.crypt :as sync-crypt]
|
||||
[frontend.worker.sync.log-and-state :as rtc-log-and-state]
|
||||
[logseq.db.frontend.schema :as db-schema]
|
||||
[promesa.core :as p]))
|
||||
(:require
|
||||
[cljs.test :refer [async deftest is]]
|
||||
[datascript.core :as d]
|
||||
[frontend.common.thread-api :as thread-api]
|
||||
[frontend.worker.a-test-env]
|
||||
[frontend.worker.db-worker :as db-worker]
|
||||
[frontend.worker.db.validate :as worker-db-validate]
|
||||
[frontend.worker.search :as search]
|
||||
[frontend.worker.shared-service :as shared-service]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[frontend.worker.sync :as db-sync]
|
||||
[frontend.worker.sync.client-op :as client-op]
|
||||
[frontend.worker.sync.crypt :as sync-crypt]
|
||||
[frontend.worker.sync.download :as sync-download]
|
||||
[frontend.worker.sync.log-and-state :as rtc-log-and-state]
|
||||
[logseq.db.frontend.schema :as db-schema]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private test-repo "test-db-worker-repo")
|
||||
(def ^:private close-db!-orig db-worker/close-db!)
|
||||
@@ -73,14 +76,56 @@
|
||||
(reset! worker-state/*opfs-pools
|
||||
{test-repo #js {:pauseVfs (fn [] (swap! pause-calls inc))}})
|
||||
(reset! search/fuzzy-search-indices {test-repo :stale-cache})
|
||||
(reset! client-op/*repo->pending-local-tx-count {test-repo 9})
|
||||
|
||||
(db-worker/close-db! test-repo)
|
||||
|
||||
(is (= #{:db :search :client-ops} (set @closed)))
|
||||
(is (= 1 @pause-calls))
|
||||
(is (nil? (get @search/fuzzy-search-indices test-repo)))
|
||||
(is (nil? (get @client-op/*repo->pending-local-tx-count test-repo)))
|
||||
(is (nil? (get @worker-state/*sqlite-conns test-repo)))))))
|
||||
|
||||
(deftest client-ops-cleanup-timer-starts-once-and-clears-on-close-test
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [scheduled (atom [])
|
||||
cleared (atom [])
|
||||
original-set-interval js/setInterval
|
||||
original-clear-interval js/clearInterval
|
||||
fake-db #js {:close (fn [] nil)}
|
||||
timer-id #js {:id "timer-1"}]
|
||||
(set! js/setInterval
|
||||
(fn [f interval-ms]
|
||||
(swap! scheduled conj {:fn f :interval-ms interval-ms})
|
||||
timer-id))
|
||||
(set! js/clearInterval
|
||||
(fn [id]
|
||||
(swap! cleared conj id)))
|
||||
(try
|
||||
(reset! worker-state/*sqlite-conns
|
||||
{test-repo {:db fake-db
|
||||
:search fake-db
|
||||
:client-ops fake-db}})
|
||||
(reset! worker-state/*datascript-conns {test-repo :datascript})
|
||||
(reset! worker-state/*client-ops-conns {test-repo :client-ops})
|
||||
(reset! (deref #'db-worker/*client-ops-cleanup-timers) {})
|
||||
|
||||
(#'db-worker/ensure-client-ops-cleanup-timer! test-repo)
|
||||
(#'db-worker/ensure-client-ops-cleanup-timer! test-repo)
|
||||
|
||||
(is (= 1 (count @scheduled)))
|
||||
(is (= (* 3 60 60 1000) (:interval-ms (first @scheduled))))
|
||||
(is (= timer-id (get @(deref #'db-worker/*client-ops-cleanup-timers) test-repo)))
|
||||
|
||||
(db-worker/close-db! test-repo)
|
||||
|
||||
(is (= [timer-id] @cleared))
|
||||
(is (nil? (get @(deref #'db-worker/*client-ops-cleanup-timers) test-repo)))
|
||||
(finally
|
||||
(set! js/setInterval original-set-interval)
|
||||
(set! js/clearInterval original-clear-interval)))))))
|
||||
|
||||
(deftest complete-datoms-import-invalidates-existing-search-db-test
|
||||
(async done
|
||||
(restoring-worker-state
|
||||
@@ -94,7 +139,7 @@
|
||||
worker-state/get-sqlite-conn (fn [_repo _type] nil)
|
||||
client-op/update-local-tx (fn [& _] nil)
|
||||
shared-service/broadcast-to-clients! (fn [& _] nil)]
|
||||
(#'db-worker/complete-datoms-import! test-repo "graph-1" 42))
|
||||
(sync-download/complete-datoms-import! test-repo "graph-1" 42))
|
||||
(p/then (fn [_]
|
||||
(is true)
|
||||
(vreset! thread-api/*thread-apis thread-apis-prev)
|
||||
@@ -114,21 +159,28 @@
|
||||
(p/resolved {:error error}))))
|
||||
|
||||
(defn- with-fake-create-or-open-db
|
||||
[repo conn f]
|
||||
(let [thread-apis-prev @thread-api/*thread-apis]
|
||||
(vreset! thread-api/*thread-apis
|
||||
(assoc thread-apis-prev
|
||||
:thread-api/create-or-open-db
|
||||
(fn [_repo _opts]
|
||||
(swap! worker-state/*datascript-conns assoc repo conn)
|
||||
(p/resolved nil))))
|
||||
(-> (f)
|
||||
(p/finally (fn []
|
||||
(vreset! thread-api/*thread-apis thread-apis-prev))))))
|
||||
|
||||
(def sample-datoms
|
||||
[{:e 1 :a :db/ident :v :logseq.class/Page :tx 1 :added true}
|
||||
{:e 2 :a :block/title :v "hello" :tx 1 :added true}])
|
||||
([repo conn f]
|
||||
(with-fake-create-or-open-db repo conn {} f))
|
||||
([repo conn
|
||||
{:keys [create-or-open-db-f close-db-f invalidate-search-db-f unlink-db-f]}
|
||||
f]
|
||||
(let [thread-apis-prev @thread-api/*thread-apis
|
||||
create-or-open-db-f (or create-or-open-db-f
|
||||
(fn [_repo _opts]
|
||||
(swap! worker-state/*datascript-conns assoc repo conn)
|
||||
(p/resolved nil)))
|
||||
close-db-f (or close-db-f (fn [_repo] nil))
|
||||
invalidate-search-db-f (or invalidate-search-db-f (fn [_repo] (p/resolved nil)))
|
||||
unlink-db-f (or unlink-db-f (fn [_repo] nil))]
|
||||
(vreset! thread-api/*thread-apis
|
||||
(assoc thread-apis-prev
|
||||
:thread-api/create-or-open-db create-or-open-db-f
|
||||
:thread-api/db-sync-close-db close-db-f
|
||||
:thread-api/db-sync-invalidate-search-db invalidate-search-db-f
|
||||
:thread-api/unsafe-unlink-db unlink-db-f))
|
||||
(-> (f)
|
||||
(p/finally (fn []
|
||||
(vreset! thread-api/*thread-apis thread-apis-prev)))))))
|
||||
|
||||
(deftest db-sync-import-prepare-replaces-active-import-state-test
|
||||
(async done
|
||||
@@ -153,82 +205,47 @@
|
||||
(is false (str error))
|
||||
(done)))))))))))
|
||||
|
||||
(deftest db-sync-import-datoms-chunk-rejects-stale-import-id-test
|
||||
(deftest db-sync-import-prepare-reset-unlinks-db-before-reopen-test
|
||||
(async done
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
|
||||
datoms-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-datoms-chunk)
|
||||
conn (d/create-conn db-schema/schema)]
|
||||
(with-fake-create-or-open-db
|
||||
test-repo conn
|
||||
(fn []
|
||||
(-> (p/with-redefs [db-worker/close-db! (fn [_] nil)
|
||||
db-worker/<invalidate-search-db! (fn [_] (p/resolved nil))
|
||||
rtc-log-and-state/rtc-log (fn [& _] nil)]
|
||||
(p/let [first-import (prepare test-repo true "graph-1" false)
|
||||
second-import (prepare test-repo true "graph-1" false)
|
||||
stale-outcome (capture-outcome #(datoms-chunk sample-datoms "graph-1" (:import-id first-import)))]
|
||||
(is (= :db-sync/stale-import (some-> stale-outcome :error ex-data :type)))
|
||||
(is (nil? (d/entity @conn 2)))
|
||||
(-> (datoms-chunk sample-datoms "graph-1" (:import-id second-import))
|
||||
(p/then (fn [_]
|
||||
(is (= "hello" (:block/title (d/entity @conn 2))))
|
||||
(done))))))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))))))
|
||||
|
||||
(deftest db-sync-import-datoms-chunk-imports-plain-datoms-to-active-db-test
|
||||
(async done
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
|
||||
datoms-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-datoms-chunk)
|
||||
conn (d/create-conn db-schema/schema)]
|
||||
(with-fake-create-or-open-db
|
||||
test-repo conn
|
||||
(fn []
|
||||
(-> (p/with-redefs [db-worker/close-db! (fn [_] nil)
|
||||
db-worker/<invalidate-search-db! (fn [_] (p/resolved nil))
|
||||
rtc-log-and-state/rtc-log (fn [& _] nil)]
|
||||
(p/let [{:keys [import-id]} (prepare test-repo true "graph-1" false)
|
||||
_ (datoms-chunk sample-datoms "graph-1" import-id)]
|
||||
(is (= :logseq.class/Page (:db/ident (d/entity @conn 1))))
|
||||
(is (= "hello" (:block/title (d/entity @conn 2))))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))))))
|
||||
|
||||
(deftest db-sync-import-datoms-chunk-imports-encrypted-datoms-to-active-db-test
|
||||
(async done
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
|
||||
datoms-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-datoms-chunk)
|
||||
conn (d/create-conn db-schema/schema)
|
||||
decrypt-calls (atom [])]
|
||||
(with-fake-create-or-open-db
|
||||
test-repo conn
|
||||
(fn []
|
||||
(-> (p/with-redefs [db-worker/close-db! (fn [_] nil)
|
||||
db-worker/<invalidate-search-db! (fn [_] (p/resolved nil))
|
||||
rtc-log-and-state/rtc-log (fn [& _] nil)
|
||||
sync-crypt/<fetch-graph-aes-key-for-download (fn [_] (p/resolved :aes-key))
|
||||
sync-crypt/<decrypt-snapshot-datoms-batch (fn [aes-key datoms]
|
||||
(swap! decrypt-calls conj {:aes-key aes-key
|
||||
:datoms datoms})
|
||||
(p/resolved datoms))]
|
||||
(p/let [{:keys [import-id]} (prepare test-repo true "graph-1" true)
|
||||
_ (datoms-chunk sample-datoms "graph-1" import-id)]
|
||||
(is (= 1 (count @decrypt-calls)))
|
||||
(is (= sample-datoms (:datoms (first @decrypt-calls))))
|
||||
(is (= "hello" (:block/title (d/entity @conn 2))))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))))))
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
|
||||
conn (d/create-conn db-schema/schema)
|
||||
calls (atom [])]
|
||||
(with-fake-create-or-open-db
|
||||
test-repo conn
|
||||
{:close-db-f (fn [repo]
|
||||
(swap! calls conj [:close repo])
|
||||
nil)
|
||||
:unlink-db-f (fn [repo]
|
||||
(swap! calls conj [:unlink repo])
|
||||
nil)
|
||||
:invalidate-search-db-f (fn [repo]
|
||||
(swap! calls conj [:invalidate-search repo])
|
||||
(p/resolved nil))
|
||||
:create-or-open-db-f (fn [repo opts]
|
||||
(swap! calls conj [:create-or-open repo opts])
|
||||
(swap! worker-state/*datascript-conns assoc repo conn)
|
||||
(p/resolved nil))}
|
||||
(fn []
|
||||
(-> (prepare test-repo true "graph-1" false)
|
||||
(p/then (fn [_]
|
||||
(let [ops (mapv first @calls)
|
||||
idx (fn [op]
|
||||
(first (keep-indexed (fn [i v]
|
||||
(when (= op v) i))
|
||||
ops)))]
|
||||
(is (some? (idx :close)))
|
||||
(is (some? (idx :unlink)))
|
||||
(is (some? (idx :invalidate-search)))
|
||||
(is (some? (idx :create-or-open)))
|
||||
(is (< (idx :close) (idx :unlink)))
|
||||
(is (< (idx :unlink) (idx :invalidate-search)))
|
||||
(is (< (idx :invalidate-search) (idx :create-or-open))))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))))))
|
||||
|
||||
(deftest db-sync-import-finalize-rejects-stale-import-id-test
|
||||
(async done
|
||||
@@ -258,33 +275,174 @@
|
||||
(is false (str error))
|
||||
(done)))))))))))
|
||||
|
||||
(deftest db-sync-import-finalize-completes-active-db-import-test
|
||||
(deftest db-sync-import-rows-chunk-calls-import-rows-batch-test
|
||||
(async done
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
|
||||
datoms-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-datoms-chunk)
|
||||
finalize (@thread-api/*thread-apis :thread-api/db-sync-import-finalize)
|
||||
rows-chunk (@thread-api/*thread-apis :thread-api/db-sync-import-rows-chunk)
|
||||
conn (d/create-conn db-schema/schema)
|
||||
search-db #js {:close (fn [] nil)
|
||||
:exec (fn [_sql] nil)}
|
||||
main-db #js {:exec (fn [_sql] nil)}]
|
||||
(reset! worker-state/*sqlite-conns {test-repo {:db main-db :search search-db :client-ops nil}})
|
||||
rows [[1 "row-1" nil]
|
||||
[2 "row-2" nil]]
|
||||
captured-rows (atom nil)]
|
||||
(with-fake-create-or-open-db
|
||||
test-repo conn
|
||||
(fn []
|
||||
(-> (p/with-redefs [db-worker/close-db! (fn [_] nil)
|
||||
db-worker/<invalidate-search-db! (fn [_] (p/resolved nil))
|
||||
db-sync/rehydrate-large-titles-from-db! (fn [& _] (p/resolved nil))
|
||||
rtc-log-and-state/rtc-log (fn [& _] nil)
|
||||
client-op/update-local-tx (fn [& _] nil)
|
||||
shared-service/broadcast-to-clients! (fn [& _] nil)]
|
||||
sync-download/<ensure-import-rows-db! (fn [state]
|
||||
(p/resolved state))
|
||||
sync-download/import-rows-batch! (fn [_state rows*]
|
||||
(reset! captured-rows rows*)
|
||||
(p/resolved 2))]
|
||||
(p/let [{:keys [import-id]} (prepare test-repo true "graph-1" false)
|
||||
_ (datoms-chunk sample-datoms "graph-1" import-id)
|
||||
_ (finalize test-repo "graph-1" 42 import-id)]
|
||||
(is (= :logseq.class/Page (:db/ident (d/entity @conn 1))))
|
||||
(is (= "hello" (:block/title (d/entity @conn 2))))
|
||||
_ (rows-chunk rows "graph-1" import-id)]
|
||||
(is (= rows @captured-rows))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))))))
|
||||
|
||||
(deftest snapshot-datoms-in-import-order-puts-schema-before-data-test
|
||||
(let [conn (d/create-conn db-schema/schema)]
|
||||
(d/transact! conn [{:db/ident :logseq.kv/schema-version
|
||||
:kv/value {:major 65 :minor 0}}
|
||||
{:db/ident :user.test/attr
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:db/id 100
|
||||
:user.test/attr "hello"}])
|
||||
(let [ordered (vec (#'sync-download/snapshot-datoms-in-import-order conn))
|
||||
data-idx (first (keep-indexed (fn [idx datom]
|
||||
(when (and (= 100 (:e datom))
|
||||
(= :user.test/attr (:a datom)))
|
||||
idx))
|
||||
ordered))
|
||||
attr-eid (:db/id (d/entity @conn :user.test/attr))
|
||||
ident-idx (first (keep-indexed (fn [idx datom]
|
||||
(when (and (= attr-eid (:e datom))
|
||||
(= :db/ident (:a datom)))
|
||||
idx))
|
||||
ordered))
|
||||
cardinality-idx (first (keep-indexed (fn [idx datom]
|
||||
(when (and (= attr-eid (:e datom))
|
||||
(= :db/cardinality (:a datom)))
|
||||
idx))
|
||||
ordered))
|
||||
schema-version-eid (:db/id (d/entity @conn :logseq.kv/schema-version))
|
||||
schema-version-idx (first (keep-indexed (fn [idx datom]
|
||||
(when (and (= schema-version-eid (:e datom))
|
||||
(= :db/ident (:a datom)))
|
||||
idx))
|
||||
ordered))]
|
||||
(is (number? data-idx))
|
||||
(is (number? ident-idx))
|
||||
(is (number? cardinality-idx))
|
||||
(is (number? schema-version-idx))
|
||||
(is (< schema-version-idx data-idx))
|
||||
(is (< ident-idx data-idx))
|
||||
(is (< cardinality-idx data-idx)))))
|
||||
|
||||
(deftest import-datoms-batch-transacts-all-db-schema-before-data-test
|
||||
(async done
|
||||
(let [conn (d/create-conn db-schema/schema)
|
||||
attr-eid 8001
|
||||
datoms [{:e attr-eid :a :db/ident :v :user.test/indexed}
|
||||
{:e 100 :a :user.test/indexed :v "hello"}
|
||||
{:e attr-eid :a :db/valueType :v :db.type/string}
|
||||
{:e attr-eid :a :db/cardinality :v :db.cardinality/one}
|
||||
{:e attr-eid :a :db/index :v true}]]
|
||||
(-> (#'sync-download/import-datoms-batch! conn nil false datoms)
|
||||
(p/then (fn [_]
|
||||
(is (= 1 (count (d/datoms @conn :avet :user.test/indexed "hello"))))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest thread-api-validate-db-passes-sync-diagnostics-test
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [validate (@thread-api/*thread-apis :thread-api/validate-db)
|
||||
conn (d/create-conn db-schema/schema)
|
||||
captured (atom nil)
|
||||
latest-prev @db-sync/*repo->latest-remote-tx]
|
||||
(reset! worker-state/*datascript-conns {test-repo conn})
|
||||
(reset! db-sync/*repo->latest-remote-tx {test-repo 11})
|
||||
(try
|
||||
(with-redefs [client-op/get-local-tx (fn [_repo] 7)
|
||||
client-op/get-local-checksum (fn [_repo] "local-checksum")
|
||||
worker-db-validate/validate-db (fn [& args]
|
||||
(reset! captured args)
|
||||
{:ok true})]
|
||||
(validate test-repo)
|
||||
(is (= [test-repo
|
||||
conn
|
||||
{:local-tx 7
|
||||
:remote-tx 11
|
||||
:local-checksum "local-checksum"
|
||||
:remote-checksum nil}]
|
||||
@captured)))
|
||||
(finally
|
||||
(reset! db-sync/*repo->latest-remote-tx latest-prev)))))))
|
||||
|
||||
(deftest thread-api-recompute-checksum-diagnostics-passes-sync-diagnostics-test
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [recompute (@thread-api/*thread-apis :thread-api/recompute-checksum-diagnostics)
|
||||
conn (d/create-conn db-schema/schema)
|
||||
captured (atom nil)
|
||||
latest-tx-prev @db-sync/*repo->latest-remote-tx
|
||||
latest-checksum-prev @db-sync/*repo->latest-remote-checksum
|
||||
result {:recomputed-checksum "recomputed"
|
||||
:checksum-attrs [:block/uuid]
|
||||
:blocks []}]
|
||||
(reset! worker-state/*datascript-conns {test-repo conn})
|
||||
(reset! db-sync/*repo->latest-remote-tx {test-repo 22})
|
||||
(reset! db-sync/*repo->latest-remote-checksum {test-repo "remote-checksum"})
|
||||
(try
|
||||
(with-redefs [client-op/get-local-tx (fn [_repo] 10)
|
||||
client-op/get-local-checksum (fn [_repo] "local-checksum")
|
||||
worker-db-validate/recompute-checksum-diagnostics (fn [& args]
|
||||
(reset! captured args)
|
||||
result)]
|
||||
(is (= (assoc result :local-checksum "recomputed")
|
||||
(recompute test-repo)))
|
||||
(is (= [test-repo
|
||||
conn
|
||||
{:local-tx 10
|
||||
:remote-tx 22
|
||||
:local-checksum "local-checksum"
|
||||
:remote-checksum "remote-checksum"}]
|
||||
@captured)))
|
||||
(finally
|
||||
(reset! db-sync/*repo->latest-remote-tx latest-tx-prev)
|
||||
(reset! db-sync/*repo->latest-remote-checksum latest-checksum-prev)))))))
|
||||
|
||||
(deftest thread-api-export-client-ops-db-checkpoints-and-exports-client-ops-file-test
|
||||
(async done
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [export-client-ops-db (@thread-api/*thread-apis :thread-api/export-client-ops-db)
|
||||
sql-calls (atom [])
|
||||
export-calls (atom [])
|
||||
expected-data (js/Uint8Array. #js [1 2 3])
|
||||
expected-buffer (.-buffer expected-data)
|
||||
fake-pool #js {:exportFile (fn [path]
|
||||
(swap! export-calls conj path)
|
||||
expected-buffer)}]
|
||||
(reset! worker-state/*opfs-pools {test-repo fake-pool})
|
||||
(with-redefs [worker-state/get-sqlite-conn (fn [_repo which-db]
|
||||
(when (= :client-ops which-db)
|
||||
#js {:exec (fn [sql]
|
||||
(swap! sql-calls conj sql))}))]
|
||||
(-> (export-client-ops-db test-repo)
|
||||
(p/then (fn [result]
|
||||
(is (= ["PRAGMA wal_checkpoint(2)"] @sql-calls))
|
||||
(is (= ["client-ops/db.sqlite"] @export-calls))
|
||||
(is (instance? js/Uint8Array result))
|
||||
(is (= [1 2 3] (vec result)))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done))))))))))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[datascript.core :as d]
|
||||
[frontend.worker.pipeline :as worker-pipeline]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.order :as db-order]
|
||||
[logseq.db.test.helper :as db-test]))
|
||||
|
||||
(deftest test-built-in-page-updates-that-should-be-reverted
|
||||
@@ -143,3 +144,46 @@
|
||||
:block/title "page1-renamed"}])]
|
||||
(is (= "page1-renamed"
|
||||
(:block/title (d/entity (:db-after result) (:db/id page1)))))))))
|
||||
|
||||
(deftest built-in-tag-must-not-convert-page-child-block-to-class-test
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "page1"}}]})
|
||||
page1 (ldb/get-page @conn "page1")
|
||||
now (js/Date.now)
|
||||
bad-block-uuid (random-uuid)
|
||||
new-tag-uuid (random-uuid)]
|
||||
(ldb/register-transact-pipeline-fn! worker-pipeline/transact-pipeline)
|
||||
|
||||
(testing "page-child block with built-in #Tag stays a block"
|
||||
(ldb/transact! conn [{:block/uuid bad-block-uuid
|
||||
:block/title "charlie"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/page (:db/id page1)
|
||||
:block/parent (:db/id page1)
|
||||
:block/order (db-order/gen-key)
|
||||
:block/tags [:logseq.class/Tag]}])
|
||||
(let [block (d/entity @conn [:block/uuid bad-block-uuid])]
|
||||
(is (some? block))
|
||||
(is (nil? (:db/ident block)))
|
||||
(is (nil? (:logseq.property.class/extends block)))
|
||||
(is (not (ldb/class? block)))
|
||||
(is (= (:db/id page1) (:db/id (:block/parent block))))
|
||||
(is (empty? (:block/tags block)))))
|
||||
|
||||
(testing "standalone candidate is still converted to a class page"
|
||||
(ldb/transact! conn [{:block/uuid new-tag-uuid
|
||||
:block/name "standalone-tag"
|
||||
:block/title "standalone-tag"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/tags [:logseq.class/Tag]}])
|
||||
(let [tag-page (d/entity @conn [:block/uuid new-tag-uuid])]
|
||||
(is (ldb/class? tag-page))
|
||||
(is (keyword? (:db/ident tag-page)))
|
||||
(is (= "user.class" (namespace (:db/ident tag-page))))
|
||||
(is (= [:logseq.class/Root]
|
||||
(map :db/ident (:logseq.property.class/extends tag-page))))))
|
||||
|
||||
;; return global fn back to previous behavior
|
||||
(ldb/register-transact-pipeline-fn! identity)))
|
||||
|
||||
@@ -19,3 +19,14 @@
|
||||
:logseq.property.reaction/target target-id}])
|
||||
affected (worker-react/get-affected-queries-keys tx-report)]
|
||||
(is (some #{[:frontend.worker.react/block-reactions target-id]} affected)))))
|
||||
|
||||
(deftest affected-keys-journals-when-journal-recycled
|
||||
(testing "recycling a journal page should refresh journals query key"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:build/journal 20240101}}
|
||||
{:page {:build/journal 20240102}}])
|
||||
journal (db-test/find-journal-by-journal-day @conn 20240102)
|
||||
tx-report (d/transact! conn [{:db/id (:db/id journal)
|
||||
:logseq.property/deleted-at 1704196800000}])
|
||||
affected (worker-react/get-affected-queries-keys tx-report)]
|
||||
(is (some #{[:frontend.worker.react/journals]} affected)))))
|
||||
|
||||
@@ -1,19 +1,122 @@
|
||||
(ns frontend.worker.sync.client-op-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[datascript.core :as d]
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[frontend.worker.sync.client-op :as client-op]))
|
||||
|
||||
(deftest update-graph-uuid-replaces-existing-value-test
|
||||
(let [repo "repo-1"
|
||||
conn (d/create-conn client-op/schema-in-db)
|
||||
(defn- new-memory-db
|
||||
[]
|
||||
(let [Database (js/require "better-sqlite3")]
|
||||
(new Database ":memory:")))
|
||||
|
||||
(defn- with-client-ops-db
|
||||
[repo f]
|
||||
(let [db (new-memory-db)
|
||||
prev-client-ops-conns @worker-state/*client-ops-conns]
|
||||
(reset! worker-state/*client-ops-conns {repo conn})
|
||||
(reset! worker-state/*client-ops-conns {repo db})
|
||||
(try
|
||||
(client-op/update-graph-uuid repo "graph-1")
|
||||
(client-op/update-graph-uuid repo "graph-2")
|
||||
(let [graph-uuid-datoms (vec (d/datoms @conn :avet :graph-uuid))]
|
||||
(is (= 1 (count graph-uuid-datoms)))
|
||||
(is (= #{"graph-2"} (set (map :v graph-uuid-datoms)))))
|
||||
(f db)
|
||||
(finally
|
||||
(.close db)
|
||||
(reset! worker-state/*client-ops-conns prev-client-ops-conns)))))
|
||||
|
||||
(defn- sqlite-count
|
||||
[^js db sql & args]
|
||||
(let [^js stmt (.prepare db sql)
|
||||
^js row (if (seq args)
|
||||
(.apply (.-get stmt) stmt (to-array args))
|
||||
(.get stmt))]
|
||||
(if row
|
||||
(or (aget row "c")
|
||||
(aget row "count"))
|
||||
0)))
|
||||
|
||||
(deftest sqlite-sync-meta-roundtrip-test
|
||||
(let [repo "repo-1"]
|
||||
(with-client-ops-db
|
||||
repo
|
||||
(fn [_db]
|
||||
(client-op/update-graph-uuid repo "graph-1")
|
||||
(client-op/update-local-tx repo 9)
|
||||
(client-op/update-local-checksum repo "checksum-1")
|
||||
|
||||
(client-op/update-graph-uuid repo "graph-2")
|
||||
(client-op/update-local-tx repo 12)
|
||||
(client-op/update-local-checksum repo "checksum-2")
|
||||
|
||||
(is (= "graph-2" (client-op/get-graph-uuid repo)))
|
||||
(is (= 12 (client-op/get-local-tx repo)))
|
||||
(is (= "checksum-2" (client-op/get-local-checksum repo)))))))
|
||||
|
||||
(deftest sqlite-asset-ops-coalescing-test
|
||||
(let [repo "repo-asset"
|
||||
asset-uuid (random-uuid)]
|
||||
(with-client-ops-db
|
||||
repo
|
||||
(fn [_db]
|
||||
(client-op/add-asset-ops repo [[:update-asset 10 {:block-uuid asset-uuid}]])
|
||||
(is (= 1 (client-op/get-unpushed-asset-ops-count repo)))
|
||||
(is (= [:update-asset 10 {:block-uuid asset-uuid}]
|
||||
(:update-asset (first (client-op/get-all-asset-ops repo)))))
|
||||
|
||||
;; older remove should be ignored because a newer update already exists
|
||||
(client-op/add-asset-ops repo [[:remove-asset 9 {:block-uuid asset-uuid}]])
|
||||
(is (= [:update-asset 10 {:block-uuid asset-uuid}]
|
||||
(:update-asset (first (client-op/get-all-asset-ops repo)))))
|
||||
|
||||
;; newer remove should replace update
|
||||
(client-op/add-asset-ops repo [[:remove-asset 11 {:block-uuid asset-uuid}]])
|
||||
(is (= [:remove-asset 11 {:block-uuid asset-uuid}]
|
||||
(:remove-asset (first (client-op/get-all-asset-ops repo)))))
|
||||
|
||||
(client-op/remove-asset-op repo asset-uuid)
|
||||
(is (= 0 (client-op/get-unpushed-asset-ops-count repo)))))))
|
||||
|
||||
(deftest cleanup-finished-history-ops-removes-only-unreferenced-finished-txs-test
|
||||
(let [repo "repo-cleanup"
|
||||
keep-tx-id (random-uuid)
|
||||
remove-tx-id (random-uuid)
|
||||
pending-tx-id (random-uuid)]
|
||||
(with-client-ops-db
|
||||
repo
|
||||
(fn [db]
|
||||
(client-op/update-local-tx repo 99)
|
||||
(client-op/upsert-local-tx-entry!
|
||||
repo
|
||||
{:tx-id keep-tx-id
|
||||
:created-at 1
|
||||
:pending? false
|
||||
:failed? false
|
||||
:normalized-tx-data []
|
||||
:reversed-tx-data []})
|
||||
(client-op/upsert-local-tx-entry!
|
||||
repo
|
||||
{:tx-id remove-tx-id
|
||||
:created-at 2
|
||||
:pending? false
|
||||
:failed? false
|
||||
:normalized-tx-data []
|
||||
:reversed-tx-data []})
|
||||
(client-op/upsert-local-tx-entry!
|
||||
repo
|
||||
{:tx-id pending-tx-id
|
||||
:created-at 3
|
||||
:pending? true
|
||||
:failed? false
|
||||
:normalized-tx-data []
|
||||
:reversed-tx-data []})
|
||||
|
||||
(is (= 1 (client-op/cleanup-finished-history-ops! repo #{keep-tx-id})))
|
||||
(is (= 1 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str keep-tx-id))))
|
||||
(is (= 0 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str remove-tx-id))))
|
||||
(is (= 1 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str pending-tx-id))))
|
||||
(is (= 99 (client-op/get-local-tx repo)))))))
|
||||
|
||||
(deftest cleanup-finished-history-ops-no-conn-is-noop-test
|
||||
(let [repo "repo-no-conn"
|
||||
prev-client-ops-conns @worker-state/*client-ops-conns]
|
||||
(reset! worker-state/*client-ops-conns {})
|
||||
(try
|
||||
(testing "cleanup should be safe when client-ops conn is missing"
|
||||
(is (= 0 (client-op/cleanup-finished-history-ops! repo #{}))))
|
||||
(finally
|
||||
(reset! worker-state/*client-ops-conns prev-client-ops-conns)))))
|
||||
|
||||
@@ -46,3 +46,85 @@
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))
|
||||
(done))))))
|
||||
|
||||
(deftest fetch-graph-aes-key-for-download-retries-with-fresh-rsa-key-pair-test
|
||||
(async done
|
||||
(let [clear-user-rsa-cache-calls* (atom 0)
|
||||
get-pair-calls* (atom 0)]
|
||||
(-> (p/with-redefs [sync-crypt/e2ee-base (fn [] "https://sync.example.test")
|
||||
sync-crypt/get-user-uuid (fn [] "user-1")
|
||||
sync-crypt/<clear-item! (fn [_] (p/resolved nil))
|
||||
sync-crypt/<set-item! (fn [_ _] (p/resolved nil))
|
||||
sync-crypt/<clear-user-rsa-key-pair-cache! (fn [_base _user-id]
|
||||
(swap! clear-user-rsa-cache-calls* inc)
|
||||
(p/resolved nil))
|
||||
sync-crypt/<get-user-rsa-key-pair-raw (fn [_base]
|
||||
(swap! get-pair-calls* inc)
|
||||
(if (= 1 @get-pair-calls*)
|
||||
(p/resolved {:public-key "pk-old"
|
||||
:encrypted-private-key "enc-old"})
|
||||
(p/resolved {:public-key "pk-new"
|
||||
:encrypted-private-key "enc-new"})))
|
||||
sync-crypt/<decrypt-private-key (fn [encrypted-private-key]
|
||||
(p/resolved
|
||||
(case encrypted-private-key
|
||||
"enc-old" :private-key-old
|
||||
"enc-new" :private-key-new
|
||||
:private-key-unknown)))
|
||||
sync-crypt/<fetch-graph-encrypted-aes-key-raw (fn [_base _graph-id]
|
||||
(p/resolved {:encrypted-aes-key
|
||||
(ldb/write-transit-str "encrypted-aes")}))
|
||||
crypt/<decrypt-aes-key (fn [private-key encrypted-aes-key]
|
||||
(if (= :private-key-old private-key)
|
||||
(p/rejected (ex-info "decrypt-aes-key" {}))
|
||||
(p/resolved [:aes-key private-key encrypted-aes-key])))]
|
||||
(sync-crypt/<fetch-graph-aes-key-for-download "graph-1"))
|
||||
(p/then (fn [result]
|
||||
(is (= [:aes-key :private-key-new "encrypted-aes"] result))
|
||||
(is (= 1 @clear-user-rsa-cache-calls*))
|
||||
(is (= 2 @get-pair-calls*))
|
||||
(done)))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))
|
||||
(done)))))))
|
||||
|
||||
(deftest fetch-graph-aes-key-for-download-rethrows-without-user-id-test
|
||||
(async done
|
||||
(let [clear-user-rsa-cache-calls* (atom 0)]
|
||||
(-> (p/with-redefs [sync-crypt/e2ee-base (fn [] "https://sync.example.test")
|
||||
sync-crypt/get-user-uuid (fn [] nil)
|
||||
sync-crypt/<clear-item! (fn [_] (p/resolved nil))
|
||||
sync-crypt/<set-item! (fn [_ _] (p/resolved nil))
|
||||
sync-crypt/<clear-user-rsa-key-pair-cache! (fn [_base _user-id]
|
||||
(swap! clear-user-rsa-cache-calls* inc)
|
||||
(p/resolved nil))
|
||||
sync-crypt/<get-user-rsa-key-pair-raw (fn [_base]
|
||||
(p/resolved {:public-key "pk-old"
|
||||
:encrypted-private-key "enc-old"}))
|
||||
sync-crypt/<decrypt-private-key (fn [_] (p/resolved :private-key-old))
|
||||
sync-crypt/<fetch-graph-encrypted-aes-key-raw (fn [_base _graph-id]
|
||||
(p/resolved {:encrypted-aes-key
|
||||
(ldb/write-transit-str "encrypted-aes")}))
|
||||
crypt/<decrypt-aes-key (fn [_ _]
|
||||
(p/rejected (ex-info "decrypt-aes-key" {})))]
|
||||
(sync-crypt/<fetch-graph-aes-key-for-download "graph-1"))
|
||||
(p/then (fn [_]
|
||||
(is false "expected decrypt-aes-key failure")
|
||||
(done)))
|
||||
(p/catch (fn [e]
|
||||
(is (= "decrypt-aes-key" (ex-message e)))
|
||||
(is (zero? @clear-user-rsa-cache-calls*))
|
||||
(done)))))))
|
||||
|
||||
(deftest decrypt-text-value-legacy-plaintext-test
|
||||
(async done
|
||||
(-> (p/let [aes-key (crypt/<generate-aes-key)
|
||||
plaintext "$$$favorites"
|
||||
encrypted (crypt/<encrypt-uint8array aes-key (.encode (js/TextEncoder.) plaintext))
|
||||
encrypted-str (ldb/write-transit-str encrypted)
|
||||
decrypted (sync-crypt/<decrypt-text-value aes-key encrypted-str)]
|
||||
(is (= plaintext decrypted))
|
||||
(done))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))
|
||||
(done))))))
|
||||
|
||||
44
src/test/frontend/worker/sync/download_test.cljs
Normal file
44
src/test/frontend/worker/sync/download_test.cljs
Normal file
@@ -0,0 +1,44 @@
|
||||
(ns frontend.worker.sync.download-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
[frontend.worker.sync.download :as sync-download]
|
||||
[logseq.db-sync.snapshot :as snapshot]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn- frame-bytes
|
||||
[^js data]
|
||||
(let [len (.-byteLength data)
|
||||
out (js/Uint8Array. (+ 4 len))
|
||||
view (js/DataView. (.-buffer out))]
|
||||
(.setUint32 view 0 len false)
|
||||
(.set out data 4)
|
||||
out))
|
||||
|
||||
(defn- stream-from-payload
|
||||
[^js payload]
|
||||
(js/ReadableStream.
|
||||
#js {:start (fn [controller]
|
||||
(.enqueue controller payload)
|
||||
(.close controller))}))
|
||||
|
||||
(deftest stream-snapshot-row-batches-ignores-stale-gzip-header-test
|
||||
(async done
|
||||
(let [rows [[1 "row-1" nil]
|
||||
[2 "row-2" nil]]
|
||||
payload (frame-bytes (snapshot/encode-rows rows))
|
||||
resp (js/Response.
|
||||
(stream-from-payload payload)
|
||||
#js {:status 200
|
||||
:headers #js {"content-encoding" "gzip"}})
|
||||
batches* (atom [])]
|
||||
(-> (#'sync-download/<stream-snapshot-row-batches!
|
||||
resp
|
||||
1000
|
||||
(fn [batch]
|
||||
(swap! batches* conj batch)
|
||||
(p/resolved true)))
|
||||
(p/then (fn [_]
|
||||
(is (= [rows] @batches*))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
1052
src/test/frontend/worker/undo_redo_test.cljs
Normal file
1052
src/test/frontend/worker/undo_redo_test.cljs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user