Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
327
deps/outliner/test/logseq/outliner/core_test.cljs
vendored
Normal file
327
deps/outliner/test/logseq/outliner/core_test.cljs
vendored
Normal file
@@ -0,0 +1,327 @@
|
||||
(ns logseq.outliner.core-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.common.config :as common-config]))
|
||||
|
||||
(deftest insert-blocks-does-not-trust-stale-right-order
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}
|
||||
{:block/title "original right"}]}])
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
stale-right-order (:block/order (db-test/find-block-by-content @conn "original right"))]
|
||||
(outliner-core/insert-blocks!
|
||||
conn
|
||||
[{:block/uuid (random-uuid)
|
||||
:block/title "concurrent right"}]
|
||||
target
|
||||
{:sibling? true
|
||||
:keep-uuid? true})
|
||||
(let [target (db-test/find-block-by-content @conn "target")
|
||||
current-right (ldb/get-right-sibling target)
|
||||
{:keys [blocks]} (outliner-core/insert-blocks
|
||||
@conn
|
||||
[{:block/uuid (random-uuid)
|
||||
:block/title "inserted"}]
|
||||
target
|
||||
{:sibling? true
|
||||
:keep-uuid? true
|
||||
:end-order-state [:known stale-right-order]})
|
||||
inserted (first blocks)]
|
||||
(is (= "concurrent right" (:block/title current-right)))
|
||||
(is (neg? (compare (:block/order inserted) (:block/order current-right)))
|
||||
"A stale renderer boundary must not place the new block after the current right sibling."))))
|
||||
|
||||
(deftest insert-blocks-finds-right-order-on-1k-sibling-page
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks (mapv (fn [idx]
|
||||
{:block/title (str "block " idx)})
|
||||
(range 1000))}])
|
||||
target (db-test/find-block-by-content @conn "block 0")
|
||||
insert! #(outliner-core/insert-blocks
|
||||
@conn
|
||||
[{:block/uuid (random-uuid)
|
||||
:block/title "inserted"}]
|
||||
target
|
||||
{:sibling? true
|
||||
:keep-uuid? true})
|
||||
_ (insert!)
|
||||
started-at (.now js/performance)
|
||||
{:keys [blocks]} (insert!)
|
||||
elapsed-ms (- (.now js/performance) started-at)
|
||||
right (ldb/get-right-sibling target)]
|
||||
(is (neg? (compare (:block/order (first blocks)) (:block/order right))))
|
||||
(is (< elapsed-ms 100)
|
||||
(str "1k sibling insertion order lookup took " elapsed-ms "ms"))))
|
||||
|
||||
(deftest blocks-with-level-handles-10k-deep-tree
|
||||
(let [blocks (mapv (fn [id]
|
||||
{:db/id id
|
||||
:block/uuid (str id)
|
||||
:block/parent {:db/id (dec id)}})
|
||||
(range 1 10001))
|
||||
started-at (.now js/performance)
|
||||
result (outliner-core/blocks-with-level blocks)
|
||||
elapsed-ms (- (.now js/performance) started-at)]
|
||||
(is (= 1 (:block/level (first result))))
|
||||
(is (= 10000 (:block/level (last result))))
|
||||
(is (< elapsed-ms 250) (str "10k level calculation took " elapsed-ms "ms"))))
|
||||
|
||||
(deftest existing-inline-class-uses-its-canonical-uuid
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"} :blocks []}])
|
||||
class-uuid (random-uuid)
|
||||
parsed-uuid (random-uuid)
|
||||
ref {:block/type "page"
|
||||
:block/name "existing"
|
||||
:block/title "Existing"
|
||||
:block/uuid parsed-uuid}
|
||||
_ (d/transact! conn [{:db/ident :user.class/existing
|
||||
:block/name "existing"
|
||||
:block/title "Existing"
|
||||
:block/uuid class-uuid
|
||||
:block/tags :logseq.class/Tag}])
|
||||
{:keys [block page-txs]}
|
||||
(#'outliner-core/resolve-page-refs
|
||||
@conn
|
||||
{:block/title (str "#[[" parsed-uuid "]]")
|
||||
:block/refs [ref]
|
||||
:block/tags [ref]})]
|
||||
(is (empty? page-txs))
|
||||
(is (= (str "#[[" class-uuid "]]") (:block/title block)))
|
||||
(is (= class-uuid (-> block :block/refs first :block/uuid)))
|
||||
(is (= class-uuid (-> block :block/tags first :block/uuid)))))
|
||||
|
||||
(deftest test-delete-block-with-default-property
|
||||
(testing "Delete block with default property hard retracts the block subtree"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:default "test block"}}]}])
|
||||
block (db-test/find-block-by-content @conn "b1")]
|
||||
(outliner-core/delete-blocks! conn [block] {})
|
||||
(is (nil? (db-test/find-block-by-content @conn "b1"))))))
|
||||
|
||||
(deftest test-delete-page-with-outliner-core
|
||||
(testing "Deleting pages through outliner-core/delete-blocks detaches page position only"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}
|
||||
{:page {:block/title "page2"}
|
||||
:blocks [{:block/title "b3"}
|
||||
{:block/title "b4"}]}])
|
||||
page2 (ldb/get-page @conn "page2")
|
||||
_ (d/transact! conn [{:db/id (:db/id page2)
|
||||
:block/order "a1"
|
||||
:block/parent (:db/id (ldb/get-page @conn "page1"))}])
|
||||
b3 (db-test/find-block-by-content @conn "b3")
|
||||
b4 (db-test/find-block-by-content @conn "b4")]
|
||||
(outliner-core/delete-blocks! conn [b3 b4 page2] {})
|
||||
(is (some? (db-test/find-block-by-content @conn "b3")))
|
||||
(is (some? (db-test/find-block-by-content @conn "b4")))
|
||||
(let [page2' (ldb/get-page @conn "page2")]
|
||||
(is (= "page2" (:block/title page2')))
|
||||
(is (nil? (:block/parent page2')))
|
||||
(is (nil? (:block/order page2')))))))
|
||||
|
||||
(deftest delete-blocks-hard-retracts-subtree
|
||||
(let [user-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}])
|
||||
parent (db-test/find-block-by-content @conn "parent")]
|
||||
(d/transact! conn [{:block/uuid user-uuid
|
||||
:block/title "Alice"}])
|
||||
(outliner-core/delete-blocks! conn [parent] {:deleted-by-uuid user-uuid})
|
||||
(let [parent' (db-test/find-block-by-content @conn "parent")
|
||||
child' (db-test/find-block-by-content @conn "child")]
|
||||
(is (nil? parent'))
|
||||
(is (nil? child')))))
|
||||
|
||||
(deftest delete-blocks-removes-range-comments-when-all-targets-are-deleted
|
||||
(let [now (js/Date.now)
|
||||
comments-uuid (random-uuid)
|
||||
comment-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target 1"}
|
||||
{:block/title "target 2"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
target-1 (db-test/find-block-by-content @conn "target 1")
|
||||
target-2 (db-test/find-block-by-content @conn "target 2")]
|
||||
(d/transact! conn [{:db/ident :logseq.class/Comments
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Comments"}
|
||||
{:db/id -1
|
||||
:block/uuid comments-uuid
|
||||
:block/title "Comments"
|
||||
:block/page (:db/id page)
|
||||
:block/parent (:db/id page)
|
||||
:block/order "a3"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/tags #{:logseq.class/Comments}
|
||||
:logseq.property.comments/blocks #{(:db/id target-1)
|
||||
(:db/id target-2)}}
|
||||
{:block/uuid comment-uuid
|
||||
:block/title "comment"
|
||||
:block/page (:db/id page)
|
||||
:block/parent -1
|
||||
:block/order "a0"
|
||||
:block/created-at now
|
||||
:block/updated-at now}])
|
||||
(outliner-core/delete-blocks! conn [target-1 target-2] {})
|
||||
(is (nil? (d/entity @conn [:block/uuid comments-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid comment-uuid])))))
|
||||
|
||||
(deftest delete-blocks-keeps-range-comments-when-some-targets-remain
|
||||
(let [now (js/Date.now)
|
||||
comments-uuid (random-uuid)
|
||||
comment-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target 1"}
|
||||
{:block/title "target 2"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
target-1 (db-test/find-block-by-content @conn "target 1")
|
||||
target-2 (db-test/find-block-by-content @conn "target 2")]
|
||||
(d/transact! conn [{:db/ident :logseq.class/Comments
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Comments"}
|
||||
{:db/id -1
|
||||
:block/uuid comments-uuid
|
||||
:block/title "Comments"
|
||||
:block/page (:db/id page)
|
||||
:block/parent (:db/id page)
|
||||
:block/order "a3"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/tags #{:logseq.class/Comments}
|
||||
:logseq.property.comments/blocks #{(:db/id target-1)
|
||||
(:db/id target-2)}}
|
||||
{:block/uuid comment-uuid
|
||||
:block/title "comment"
|
||||
:block/page (:db/id page)
|
||||
:block/parent -1
|
||||
:block/order "a0"
|
||||
:block/created-at now
|
||||
:block/updated-at now}])
|
||||
(outliner-core/delete-blocks! conn [target-1] {})
|
||||
(is (some? (d/entity @conn [:block/uuid comments-uuid])))
|
||||
(is (some? (d/entity @conn [:block/uuid comment-uuid])))
|
||||
(is (some? (d/entity @conn (:db/id target-2))))))
|
||||
|
||||
(deftest delete-blocks-rejects-built-in-entities
|
||||
(let [conn (db-test/create-conn)]
|
||||
(testing "built-in page is rejected"
|
||||
(let [recycle-page (ldb/get-page @conn common-config/recycle-page-name)]
|
||||
(is (true? (:logseq.property/built-in? recycle-page)))
|
||||
(is (thrown-with-msg? js/Error #"Built-in nodes can't be deleted"
|
||||
(db-test/silence-stderr (outliner-core/delete-blocks! conn [recycle-page] {}))))))
|
||||
|
||||
(testing "built-in idents that are not a class or property like empty-placeholder are rejected"
|
||||
(let [placeholder (d/entity @conn :logseq.property/empty-placeholder)]
|
||||
(is (some? (:block/uuid placeholder)))
|
||||
(is (thrown-with-msg? js/Error #"Built-in nodes can't be deleted"
|
||||
(db-test/silence-stderr (outliner-core/delete-blocks! conn [placeholder] {}))))))
|
||||
|
||||
(testing "file entity is rejected"
|
||||
(let [file (->> (d/datoms @conn :avet :file/path)
|
||||
first
|
||||
:e
|
||||
(d/entity @conn))]
|
||||
(is (some? (:file/path file)))
|
||||
(is (thrown-with-msg? js/Error #"Built-in nodes can't be deleted"
|
||||
(db-test/silence-stderr (outliner-core/delete-blocks! conn [file] {}))))))
|
||||
|
||||
(testing "KV entity is rejected"
|
||||
(let [kv (d/entity @conn :logseq.kv/db-type)]
|
||||
(is (some? (:db/id kv)))
|
||||
(is (thrown-with-msg? js/Error #"Built-in nodes can't be deleted"
|
||||
(db-test/silence-stderr (outliner-core/delete-blocks! conn [kv] {}))))))))
|
||||
|
||||
(deftest save-block-rejects-built-in-entity
|
||||
(let [conn (db-test/create-conn)
|
||||
placeholder (d/entity @conn :logseq.property/description)]
|
||||
(is (thrown-with-msg? js/Error #"Built-in.*can't be modified"
|
||||
(db-test/silence-stderr
|
||||
(outliner-core/save-block! conn {:db/id (:db/id placeholder) :block/title "hacked"}))))))
|
||||
|
||||
(deftest move-blocks-rejects-built-in-entity
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}])
|
||||
placeholder (d/entity @conn :logseq.property/description)
|
||||
target (db-test/find-block-by-content @conn "target")]
|
||||
(is (thrown-with-msg? js/Error #"Built-in.*can't be modified"
|
||||
(db-test/silence-stderr
|
||||
(outliner-core/move-blocks! conn [placeholder] target {:sibling? true}))))))
|
||||
|
||||
(deftest move-blocks-protects-comment-blocks
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}
|
||||
{:block/title "tagged comment"}
|
||||
{:block/title "normal parent"
|
||||
:build/children [{:block/title "ordinary"}]}
|
||||
{:block/title "Comments"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
tagged-comment (db-test/find-block-by-content @conn "tagged comment")
|
||||
ordinary (db-test/find-block-by-content @conn "ordinary")
|
||||
comments-area (db-test/find-block-by-content @conn "Comments")
|
||||
original-comment-parent-id (:db/id (:block/parent tagged-comment))]
|
||||
(d/transact! conn [{:db/id -1
|
||||
:db/ident :logseq.class/Tag
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Tag"
|
||||
:block/tags #{-1}}
|
||||
{:db/ident :logseq.class/Comments
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Comments"
|
||||
:block/tags #{-1}}
|
||||
{:db/ident :logseq.class/Comment
|
||||
:block/uuid (random-uuid)
|
||||
:block/title "Comment"
|
||||
:block/tags #{-1}}
|
||||
[:db/add (:db/id comments-area) :block/tags :logseq.class/Comments]
|
||||
[:db/add (:db/id tagged-comment) :block/tags :logseq.class/Comment]])
|
||||
|
||||
(testing "#Comment blocks cannot be moved after creation"
|
||||
(outliner-core/move-blocks! conn [tagged-comment] target {:sibling? false})
|
||||
(is (= original-comment-parent-id
|
||||
(:db/id (:block/parent (d/entity @conn (:db/id tagged-comment)))))))
|
||||
|
||||
(testing "#Comments blocks cannot be moved as children"
|
||||
(outliner-core/move-blocks! conn [comments-area] target {:sibling? false})
|
||||
(let [comments-area' (d/entity @conn (:db/id comments-area))]
|
||||
(is (= (:db/id page)
|
||||
(:db/id (:block/parent comments-area'))))))
|
||||
|
||||
(testing "#Comments blocks can be moved as siblings"
|
||||
(outliner-core/move-blocks! conn [comments-area] target {:sibling? true})
|
||||
(let [page' (d/entity @conn (:db/id page))
|
||||
children (->> (:block/_parent page')
|
||||
ldb/sort-by-order
|
||||
(mapv :block/title))]
|
||||
(is (= ["target" "Comments" "tagged comment" "normal parent"] children))))
|
||||
|
||||
(testing "ordinary blocks can be moved as siblings of #Comments blocks"
|
||||
(outliner-core/move-blocks! conn [ordinary] comments-area {:sibling? true})
|
||||
(let [ordinary' (d/entity @conn (:db/id ordinary))]
|
||||
(is (= (:db/id page)
|
||||
(:db/id (:block/parent ordinary'))))))
|
||||
|
||||
(testing "ordinary blocks cannot be moved as children of #Comments blocks"
|
||||
(outliner-core/move-blocks! conn [ordinary] comments-area {:sibling? false})
|
||||
(is (= (:db/id page)
|
||||
(:db/id (:block/parent (d/entity @conn (:db/id ordinary)))))))
|
||||
|
||||
(testing "ordinary blocks cannot be moved to #Comment blocks"
|
||||
(outliner-core/move-blocks! conn [ordinary] tagged-comment {:sibling? false})
|
||||
(is (= (:db/id page)
|
||||
(:db/id (:block/parent (d/entity @conn (:db/id ordinary)))))))))
|
||||
530
deps/outliner/test/logseq/outliner/op_construct_test.cljs
vendored
Normal file
530
deps/outliner/test/logseq/outliner/op_construct_test.cljs
vendored
Normal file
@@ -0,0 +1,530 @@
|
||||
(ns logseq.outliner.op-construct-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.op.construct :as op-construct]))
|
||||
|
||||
(defn- run-direct-outdent
|
||||
[conn block]
|
||||
(let [{:keys [tx-data]}
|
||||
(#'outliner-core/indent-outdent-blocks
|
||||
conn [block] false
|
||||
:parent-original nil
|
||||
:logical-outdenting? nil)
|
||||
tx-report (d/with @conn tx-data {})]
|
||||
{:tx-data (:tx-data tx-report)
|
||||
:db-after (:db-after tx-report)}))
|
||||
|
||||
(deftest derive-history-outliner-ops-canonicalizes-create-page-and-builds-delete-inverse-test
|
||||
(testing "create-page forward op keeps created uuid and reverse op deletes that page"
|
||||
(let [conn (db-test/create-conn-with-blocks {:pages-and-blocks []})
|
||||
page-uuid (random-uuid)
|
||||
tx-data [{:e 1 :a :block/title :v "Created Page" :added true}
|
||||
{:e 1 :a :block/uuid :v page-uuid :added true}]
|
||||
tx-meta {:outliner-op :create-page
|
||||
:outliner-ops [[:create-page ["Created Page"
|
||||
{:redirect? false
|
||||
:split-namespace? true
|
||||
:tags ()}]]]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn tx-data tx-meta)]
|
||||
(is (= :create-page (ffirst forward-outliner-ops)))
|
||||
(is (= page-uuid (get-in forward-outliner-ops [0 1 1 :uuid])))
|
||||
(is (= [[:delete-page [page-uuid {}]]]
|
||||
inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-handles-replace-empty-target-insert-inverse-test
|
||||
(testing "replace-empty-target insert keeps source uuid and inverse deletes target placeholder"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title ""}]}]})
|
||||
empty-target (db-test/find-block-by-content @conn "")
|
||||
parent-uuid (random-uuid)
|
||||
child-uuid (random-uuid)
|
||||
tx-meta {:outliner-op :insert-blocks
|
||||
:outliner-ops [[:insert-blocks [[{:block/uuid parent-uuid
|
||||
:block/title "paste parent"}
|
||||
{:block/uuid child-uuid
|
||||
:block/title "paste child"
|
||||
:block/parent [:block/uuid parent-uuid]}]
|
||||
(:db/id empty-target)
|
||||
{:sibling? true
|
||||
:replace-empty-target? true
|
||||
:outliner-op :paste}]]]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= parent-uuid
|
||||
(get-in forward-outliner-ops [0 1 0 0 :block/uuid])))
|
||||
(is (= true (get-in forward-outliner-ops [0 1 2 :keep-uuid?])))
|
||||
(is (some #(and (= :delete-blocks (first %))
|
||||
(= [(:block/uuid empty-target)]
|
||||
(vec (get-in % [1 0]))))
|
||||
(remove nil? inverse-outliner-ops)))
|
||||
(is (not-any? #(= :save-block (first %))
|
||||
(remove nil? inverse-outliner-ops))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-builds-upsert-property-inverse-delete-page-test
|
||||
(testing "upsert-property with qualified keyword builds delete-page inverse"
|
||||
(let [conn (db-test/create-conn-with-blocks {:pages-and-blocks []})
|
||||
property-id :user.property/custom-prop
|
||||
tx-meta {:outliner-op :upsert-property
|
||||
:outliner-ops [[:upsert-property [property-id
|
||||
{:logseq.property/type :default}
|
||||
{:property-name "custom-prop"}]]]}
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)
|
||||
expected-page-uuid (common-uuid/gen-uuid :db-ident-block-uuid property-id)]
|
||||
(is (= [[:delete-page [expected-page-uuid {}]]]
|
||||
inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-upsert-property-update-builds-schema-restore-inverse-test
|
||||
(testing "upsert-property on existing property builds inverse upsert-property restore op"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:p-many {:logseq.property/type :default}}
|
||||
:pages-and-blocks []})
|
||||
property-id :user.property/p-many
|
||||
_ (d/transact! conn [[:db/add property-id :logseq.property/classes :logseq.class/Root]])
|
||||
before-property (d/entity @conn property-id)
|
||||
expected-schema (-> (db-property/get-property-schema (into {} before-property))
|
||||
(update :logseq.property/classes
|
||||
(fn [classes]
|
||||
(some->> classes
|
||||
(map (fn [class]
|
||||
(if-let [class-uuid (:block/uuid class)]
|
||||
[:block/uuid class-uuid]
|
||||
(:db/ident class))))
|
||||
set))))
|
||||
tx-meta {:outliner-op :upsert-property
|
||||
:outliner-ops [[:upsert-property [property-id
|
||||
{:logseq.property/type :node
|
||||
:db/cardinality :many}
|
||||
{}]]]}
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= [[:upsert-property [property-id expected-schema {:property-name "p-many"}]]]
|
||||
inverse-outliner-ops))
|
||||
(is (every? (fn [class-ref]
|
||||
(or (keyword? class-ref)
|
||||
(and (vector? class-ref)
|
||||
(= :block/uuid (first class-ref))
|
||||
(uuid? (second class-ref)))))
|
||||
(get-in inverse-outliner-ops [0 1 1 :logseq.property/classes]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-blocks-inverse-avoids-self-target-test
|
||||
(testing "delete-blocks inverse falls back to parent target when left sibling resolves to self"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
child-id (:db/id child)
|
||||
child-uuid (:block/uuid child)
|
||||
parent-uuid (some-> child :block/parent :block/uuid)
|
||||
tx-meta {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:delete-blocks [[child-id] {}]]]}]
|
||||
;; Simulate stale sibling lookup returning the same entity as the deleted root.
|
||||
(with-redefs [ldb/get-left-sibling (fn [_] child)]
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)
|
||||
insert-op (first inverse-outliner-ops)]
|
||||
(is (= :insert-blocks (first insert-op)))
|
||||
(is (= parent-uuid
|
||||
(get-in insert-op [1 1])))
|
||||
(is (= false (get-in insert-op [1 2 :sibling?])))
|
||||
(is (not= child-uuid
|
||||
(get-in insert-op [1 1]))))))))
|
||||
|
||||
(deftest compound-history-inverses-run-in-reverse-dependency-order-test
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "target"}
|
||||
{:block/title "source"
|
||||
:build/children [{:block/title "nested"}]}]}]})
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
source (db-test/find-block-by-content @conn "source")
|
||||
nested (db-test/find-block-by-content @conn "nested")
|
||||
tx-meta {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:move-blocks [[(:db/id nested)]
|
||||
(:db/id target)
|
||||
{:sibling? false}]]
|
||||
[:delete-blocks [[(:db/id source)] {}]]
|
||||
[:save-block [(assoc (into {} target) :block/title "combined") nil]]]}
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= [:save-block :insert-blocks :move-blocks]
|
||||
(mapv first inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-blocks-with-stale-id-keeps-id-test
|
||||
(testing "delete-blocks derive-history keeps unresolved numeric ids in forward ops"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
child-uuid (:block/uuid child)
|
||||
stale-id 99999999
|
||||
tx-meta {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:delete-blocks [[(:db/id child) stale-id] {}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn [] tx-meta)]
|
||||
(is (= [[:delete-blocks [[child-uuid stale-id] {}]]]
|
||||
forward-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-delete-blocks-prefers-retracted-tx-data-ids-test
|
||||
(testing "delete-blocks derive-history should prefer tx-data retractEntity ids over stale selection ids"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
child-id (:db/id child)
|
||||
child-uuid (:block/uuid child)
|
||||
stale-id 99999999
|
||||
tx-report (d/with @conn [[:db/retractEntity child-id]] {})
|
||||
tx-meta {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:delete-blocks [[child-id stale-id] {}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn (:db-after tx-report) (:tx-data tx-report) tx-meta)]
|
||||
(is (= [[:delete-blocks [[child-uuid] {}]]]
|
||||
forward-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-move-blocks-resolves-target-id-from-tx-data-test
|
||||
(testing "move-blocks should resolve stale numeric target id using tx-data block uuid"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "child"}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
stale-target-id 9999999
|
||||
target-uuid (random-uuid)
|
||||
tx-data [{:e stale-target-id :a :block/uuid :v target-uuid :added false}]
|
||||
tx-meta {:outliner-op :move-blocks
|
||||
:outliner-ops [[:move-blocks [[(:db/id child)] stale-target-id {:sibling? true}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn tx-data tx-meta)]
|
||||
(is (= target-uuid
|
||||
(get-in forward-outliner-ops [0 1 1]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-save-block-resolves-retracted-ref-id-from-db-before-test
|
||||
(testing "save-block should canonicalize ref attrs using db-before when target ref is retracted in current tx"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "child"}]}]})
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
child-uuid (:block/uuid child)
|
||||
tag-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:db/id -1
|
||||
:block/uuid tag-uuid
|
||||
:block/title "Tag"
|
||||
:block/name "tag"
|
||||
:block/tags :logseq.class/Tag}])
|
||||
tag-id (:db/id (d/entity @conn [:block/uuid tag-uuid]))
|
||||
tx-report (d/with @conn [[:db/retractEntity tag-id]] {})
|
||||
tx-meta {:outliner-op :save-block
|
||||
:outliner-ops [[:save-block [{:block/uuid child-uuid
|
||||
:block/tags #{tag-id}}
|
||||
{}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn (:db-after tx-report) (:tx-data tx-report) tx-meta)]
|
||||
(is (= #{[:block/uuid tag-uuid]}
|
||||
(get-in forward-outliner-ops [0 1 0 :block/tags]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-insert-blocks-resolves-retracted-ref-id-from-tx-data-test
|
||||
(testing "insert-blocks should canonicalize block ref attrs when ref entity is retracted in current tx"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "target"}]}]})
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
target-id (:db/id target)
|
||||
tag-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:db/id -1
|
||||
:block/uuid tag-uuid
|
||||
:block/title "Tag"
|
||||
:block/name "tag"
|
||||
:block/tags :logseq.class/Tag}])
|
||||
tag-id (:db/id (d/entity @conn [:block/uuid tag-uuid]))
|
||||
tx-report (d/with @conn [[:db/retractEntity tag-id]] {})
|
||||
tx-meta {:outliner-op :insert-blocks
|
||||
:outliner-ops [[:insert-blocks [[{:block/uuid (random-uuid)
|
||||
:block/title "new child"
|
||||
:block/tags #{tag-id}}]
|
||||
target-id
|
||||
{:sibling? true}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn (:db-after tx-report) (:tx-data tx-report) tx-meta)]
|
||||
(is (= #{[:block/uuid tag-uuid]}
|
||||
(get-in forward-outliner-ops [0 1 0 0 :block/tags]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-apply-template-undo-canonicalizes-template-block-refs-test
|
||||
(testing "apply-template undo explicit inverse should canonicalize nested template-block ref attrs"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "template"}
|
||||
{:block/title "target"}]}]})
|
||||
template (db-test/find-block-by-content @conn "template")
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
template-uuid (:block/uuid template)
|
||||
target-uuid (:block/uuid target)
|
||||
inserted-uuid (random-uuid)
|
||||
tag-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:db/id -1
|
||||
:block/uuid tag-uuid
|
||||
:block/title "Tag"
|
||||
:block/name "tag"
|
||||
:block/tags :logseq.class/Tag}])
|
||||
tag-id (:db/id (d/entity @conn [:block/uuid tag-uuid]))
|
||||
tx-report (d/with @conn [[:db/retractEntity tag-id]] {})
|
||||
tx-meta {:outliner-op :apply-template
|
||||
:undo? true
|
||||
:db-sync/inverse-outliner-ops
|
||||
[[:apply-template [template-uuid
|
||||
target-uuid
|
||||
{:template-blocks [{:block/uuid inserted-uuid
|
||||
:block/title "inserted"
|
||||
:block/tags #{tag-id}}]
|
||||
:sibling? true}]]]}
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn (:db-after tx-report) (:tx-data tx-report) tx-meta)]
|
||||
(is (= #{[:block/uuid tag-uuid]}
|
||||
(get-in inverse-outliner-ops [0 1 2 :template-blocks 0 :block/tags]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-apply-template-captures-template-blocks-when-missing-in-op-test
|
||||
(testing "apply-template forward op should capture concrete template-blocks even if original op omitted them"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "template"
|
||||
:build/children [{:block/title "template child 1"}
|
||||
{:block/title "template child 2"}]}
|
||||
{:block/title "target"}]}]})
|
||||
template (db-test/find-block-by-content @conn "template")
|
||||
template-child-1 (db-test/find-block-by-content @conn "template child 1")
|
||||
template-child-2 (db-test/find-block-by-content @conn "template child 2")
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
inserted-child-1-uuid (random-uuid)
|
||||
inserted-child-2-uuid (random-uuid)
|
||||
tx-data [{:e 900001 :a :block/uuid :v inserted-child-1-uuid :added true}
|
||||
{:e 900002 :a :block/uuid :v inserted-child-2-uuid :added true}]
|
||||
_ (d/transact! conn [[:db/add (:db/id template-child-1) :block/refs (:db/id template-child-2)]])
|
||||
tx-meta {:outliner-op :apply-template
|
||||
:outliner-ops [[:apply-template [(:db/id template)
|
||||
(:db/id target)
|
||||
{:sibling? true}]]]}
|
||||
{:keys [forward-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn @conn tx-data tx-meta)]
|
||||
(is (= :apply-template (ffirst forward-outliner-ops)))
|
||||
(is (= true (get-in forward-outliner-ops [0 1 2 :keep-uuid?])))
|
||||
(is (= [inserted-child-1-uuid inserted-child-2-uuid]
|
||||
(mapv :block/uuid (get-in forward-outliner-ops [0 1 2 :template-blocks]))))
|
||||
(is (= #{[:block/uuid inserted-child-2-uuid]}
|
||||
(get-in forward-outliner-ops [0 1 2 :template-blocks 0 :block/refs]))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-builds-delete-page-inverse-for-class-property-and-today-page-test
|
||||
(testing "delete-page inverse restores hard-retracted class/property/today pages with stable db/ident"
|
||||
(let [today (date-time-util/ms->journal-day (js/Date.))
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:classes {:Movie {}}
|
||||
:properties {:rating {:logseq.property/type :number}}
|
||||
:pages-and-blocks [{:page {:build/journal today}
|
||||
:blocks [{:block/title "today child"}]}]})
|
||||
class-page (ldb/get-page @conn "Movie")
|
||||
property-page (d/entity @conn :user.property/rating)
|
||||
today-page (db-test/find-journal-by-journal-day @conn today)
|
||||
today-child (db-test/find-block-by-content @conn "today child")
|
||||
class-inverse (:inverse-outliner-ops
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-page
|
||||
:outliner-ops [[:delete-page [(:block/uuid class-page) {}]]]}))
|
||||
property-inverse (:inverse-outliner-ops
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-page
|
||||
:outliner-ops [[:delete-page [(:block/uuid property-page) {}]]]}))
|
||||
today-inverse (:inverse-outliner-ops
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-page
|
||||
:outliner-ops [[:delete-page [(:block/uuid today-page) {}]]]}))]
|
||||
(is (some #(= :create-page (first %)) class-inverse))
|
||||
(is (some #(= :save-block (first %)) class-inverse))
|
||||
(is (= (:db/ident class-page)
|
||||
(get-in (some #(when (= :save-block (first %)) %) class-inverse)
|
||||
[1 0 :db/ident])))
|
||||
|
||||
(is (some #(= :upsert-property (first %)) property-inverse))
|
||||
(is (some #(= :save-block (first %)) property-inverse))
|
||||
(is (= (:db/ident property-page)
|
||||
(get-in (some #(when (= :save-block (first %)) %) property-inverse)
|
||||
[1 0 :db/ident])))
|
||||
|
||||
(is (not-any? #(= :restore-recycled (first %)) today-inverse))
|
||||
(let [today-insert-op (some #(when (= :insert-blocks (first %)) %) today-inverse)]
|
||||
(is (some? today-insert-op))
|
||||
(is (= (:block/uuid today-page)
|
||||
(get-in today-insert-op [1 1])))
|
||||
(is (= (:block/uuid today-child)
|
||||
(get-in today-insert-op [1 0 0 :block/uuid])))))))
|
||||
|
||||
(deftest derive-history-outliner-ops-builds-inverse-for-all-supported-ops-test
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:c1 {:build/class-properties [:p1]}}
|
||||
:properties {:p1 {:logseq.property/type :default}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child-a"}
|
||||
{:block/title "child-b"}]}
|
||||
{:block/title "prop-block-1"
|
||||
:build/properties {:p1 "before-1"}}
|
||||
{:block/title "prop-block-2"}]}]})
|
||||
page (db-test/find-page-by-title @conn "page")
|
||||
parent (db-test/find-block-by-content @conn "parent")
|
||||
child-a (db-test/find-block-by-content @conn "child-a")
|
||||
child-b (db-test/find-block-by-content @conn "child-b")]
|
||||
(testing ":save-block"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :save-block
|
||||
:outliner-ops [[:save-block [{:block/uuid (:block/uuid child-a)
|
||||
:block/title "changed"} {}]]]})]
|
||||
(is (= :save-block (ffirst inverse-outliner-ops)))
|
||||
(is (= (:block/uuid child-a)
|
||||
(get-in inverse-outliner-ops [0 1 0 :block/uuid])))))
|
||||
|
||||
(testing ":insert-blocks"
|
||||
(let [inserted-uuid (random-uuid)
|
||||
tx-data [{:e 999999 :a :block/uuid :v inserted-uuid :added true}]
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn tx-data {:outliner-op :insert-blocks
|
||||
:outliner-ops [[:insert-blocks [[{:block/uuid inserted-uuid
|
||||
:block/title "new"}]
|
||||
(:db/id parent)
|
||||
{:sibling? false}]]]})]
|
||||
(is (= :delete-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= [inserted-uuid]
|
||||
(vec (get-in inverse-outliner-ops [0 1 0]))))))
|
||||
|
||||
(testing ":move-blocks"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :move-blocks
|
||||
:outliner-ops [[:move-blocks [[(:db/id child-b)]
|
||||
(:db/id parent)
|
||||
{:sibling? false}]]]})]
|
||||
(is (= :move-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= [(:block/uuid child-b)]
|
||||
(get-in inverse-outliner-ops [0 1 0])))))
|
||||
|
||||
(testing ":delete-blocks"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-blocks
|
||||
:outliner-ops [[:delete-blocks [[(:db/id child-b)] {}]]]})]
|
||||
(is (= :insert-blocks (ffirst inverse-outliner-ops)))
|
||||
(is (= (:block/uuid child-b)
|
||||
(get-in inverse-outliner-ops [0 1 0 0 :block/uuid])))))
|
||||
|
||||
(testing ":create-page"
|
||||
(let [page-uuid (random-uuid)
|
||||
tx-data [{:e 1 :a :block/title :v "P2" :added true}
|
||||
{:e 1 :a :block/uuid :v page-uuid :added true}]
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn tx-data {:outliner-op :create-page
|
||||
:outliner-ops [[:create-page ["P2" {:redirect? false}]]]})]
|
||||
(is (= [[:delete-page [page-uuid {}]]]
|
||||
inverse-outliner-ops))))
|
||||
|
||||
(testing ":delete-page"
|
||||
(let [{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :delete-page
|
||||
:outliner-ops [[:delete-page [(:block/uuid page) {}]]]})]
|
||||
(is (= [[:restore-recycled [(:block/uuid page)]]]
|
||||
inverse-outliner-ops))))
|
||||
|
||||
(testing ":upsert-property"
|
||||
(let [property-ident :user.property/test-inverse
|
||||
expected-page-uuid (common-uuid/gen-uuid :db-ident-block-uuid property-ident)
|
||||
{:keys [inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops
|
||||
@conn @conn [] {:outliner-op :upsert-property
|
||||
:outliner-ops [[:upsert-property [property-ident
|
||||
{:logseq.property/type :default}
|
||||
{:property-name "test-inverse"}]]]})]
|
||||
(is (= [[:delete-page [expected-page-uuid {}]]]
|
||||
inverse-outliner-ops))))))
|
||||
|
||||
(deftest build-history-action-metadata-direct-outdent-builds-indent-outdent-forward-and-inverse-test
|
||||
(testing "direct outdent keeps canonical indent-outdent forward and inverse ops"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child-1"}
|
||||
{:block/title "child-2"}
|
||||
{:block/title "child-3"}]}]}]})
|
||||
child-3 (db-test/find-block-by-content @conn "child-3")
|
||||
{:keys [tx-data db-after]} (run-direct-outdent conn child-3)
|
||||
tx-meta {:outliner-op :move-blocks
|
||||
:outliner-ops [[:indent-outdent-blocks [[(:db/id child-3)]
|
||||
false
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn db-after tx-data tx-meta)]
|
||||
(is (= [[:indent-outdent-blocks [[(:block/uuid child-3)]
|
||||
false
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]
|
||||
forward-outliner-ops))
|
||||
(is (= [[:indent-outdent-blocks [[(:block/uuid child-3)]
|
||||
true
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]
|
||||
inverse-outliner-ops)))))
|
||||
|
||||
(deftest derive-history-outliner-ops-direct-outdent-with-extra-moved-blocks-keeps-semantic-ops-test
|
||||
(testing "direct outdent keeps semantic indent-outdent op and inverse"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child-1"}
|
||||
{:block/title "child-2"}
|
||||
{:block/title "child-3"}]}]}]})
|
||||
child-2 (db-test/find-block-by-content @conn "child-2")
|
||||
{:keys [tx-data db-after]} (run-direct-outdent conn child-2)
|
||||
tx-meta {:outliner-op :move-blocks
|
||||
:outliner-ops [[:indent-outdent-blocks [[(:db/id child-2)]
|
||||
false
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]}
|
||||
{:keys [forward-outliner-ops inverse-outliner-ops]}
|
||||
(op-construct/derive-history-outliner-ops @conn db-after tx-data tx-meta)]
|
||||
(is (= [[:indent-outdent-blocks [[(:block/uuid child-2)]
|
||||
false
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]
|
||||
forward-outliner-ops))
|
||||
(is (= [[:indent-outdent-blocks [[(:block/uuid child-2)]
|
||||
true
|
||||
{:parent-original nil
|
||||
:logical-outdenting? nil}]]]
|
||||
inverse-outliner-ops)))))
|
||||
251
deps/outliner/test/logseq/outliner/op_test.cljs
vendored
Normal file
251
deps/outliner/test/logseq/outliner/op_test.cljs
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
(ns logseq.outliner.op-test
|
||||
(:require [clojure.string :as string]
|
||||
[cljs.test :refer [deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.util.page-ref :as page-ref]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.op :as outliner-op]
|
||||
[logseq.outliner.property :as outliner-property]))
|
||||
|
||||
(deftest toggle-reaction-op
|
||||
(testing "toggles reactions via outliner ops"
|
||||
(let [user-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
now 1234]
|
||||
(ldb/transact! conn
|
||||
[{:block/uuid user-uuid
|
||||
:block/name "user"
|
||||
:block/title "user"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/tags #{:logseq.class/Page}}]
|
||||
{})
|
||||
(let [block (db-test/find-block-by-content @conn "Block")
|
||||
target-uuid (:block/uuid block)]
|
||||
(outliner-op/apply-ops! conn
|
||||
[[:toggle-reaction [target-uuid "+1" user-uuid]]]
|
||||
{})
|
||||
(let [block-entity (d/entity @conn [:block/uuid target-uuid])
|
||||
reactions (:logseq.property.reaction/_target block-entity)
|
||||
reaction (first reactions)]
|
||||
(is (= 1 (count reactions)))
|
||||
(is (uuid? (:block/uuid reaction)))
|
||||
(is (= "+1" (:logseq.property.reaction/emoji-id reaction)))
|
||||
(is (= (:db/id (d/entity @conn [:block/uuid user-uuid]))
|
||||
(:db/id (:logseq.property/created-by-ref reaction)))))
|
||||
(outliner-op/apply-ops! conn
|
||||
[[:toggle-reaction [target-uuid "+1" user-uuid]]]
|
||||
{})
|
||||
(let [block-entity (d/entity @conn [:block/uuid target-uuid])]
|
||||
(is (empty? (:logseq.property.reaction/_target block-entity))))))))
|
||||
|
||||
(deftest collapse-expand-blocks-op
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Parent"}]}])
|
||||
block (db-test/find-block-by-content @conn "Parent")
|
||||
block-id (:block/uuid block)]
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:collapse-expand-blocks [[{:block/uuid block-id
|
||||
:block/collapsed? true}]
|
||||
{}]]]
|
||||
{})
|
||||
(is (true? (:block/collapsed? (d/entity @conn [:block/uuid block-id]))))
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:collapse-expand-blocks [[{:block/uuid block-id
|
||||
:block/collapsed? false}]
|
||||
{}]]]
|
||||
{})
|
||||
(is (false? (:block/collapsed? (d/entity @conn [:block/uuid block-id]))))))
|
||||
|
||||
(deftest resolve-indent-outdent-parent-original-test
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Original"}]}])
|
||||
original (db-test/find-block-by-content @conn "Original")
|
||||
opts (#'outliner-op/resolve-indent-outdent-opts
|
||||
@conn
|
||||
{:parent-original {:block/uuid (:block/uuid original)}})]
|
||||
(is (= (:db/id original) (get-in opts [:parent-original :db/id])))))
|
||||
|
||||
(deftest apply-ops-plugin-property-sequence-test
|
||||
(testing "plugin property ops remain visible after a single apply-ops! batch"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
block (db-test/find-block-by-content @conn "Block")
|
||||
block-uuid (:block/uuid block)]
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:upsert-property [:plugin.property._test_plugin/x1 {:logseq.property/type :checkbox
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x1}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x1 true]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x2 {:logseq.property/type :url
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x2}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x2 "https://logseq.com"]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x3 {:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x3}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x3 1]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x4 {:logseq.property/type :number
|
||||
:db/cardinality :db.cardinality/many}
|
||||
{:property-name :x4}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x4 1]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x5 {:logseq.property/type :json
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x5}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x5 "{\"foo\":\"bar\"}"]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x6 {:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x6}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x6 "Page x"]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x7 {:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/many}
|
||||
{:property-name :x7}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x7 "Page y"]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x7 "Page z"]]
|
||||
[:upsert-property [:plugin.property._test_plugin/x8 {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :x8}]]
|
||||
[:set-block-property [block-uuid :plugin.property._test_plugin/x8 "some content"]]]
|
||||
{})
|
||||
(let [block' (d/entity @conn [:block/uuid block-uuid])]
|
||||
(is (true? (:plugin.property._test_plugin/x1 block')))
|
||||
(is (= "https://logseq.com"
|
||||
(:block/title (:plugin.property._test_plugin/x2 block'))))
|
||||
(is (= 1
|
||||
(:logseq.property/value (:plugin.property._test_plugin/x3 block'))))
|
||||
(is (= #{1}
|
||||
(set (map :logseq.property/value (:plugin.property._test_plugin/x4 block')))))
|
||||
(is (= "{\"foo\":\"bar\"}" (:plugin.property._test_plugin/x5 block')))
|
||||
(is (= "page x"
|
||||
(:block/name (:plugin.property._test_plugin/x6 block'))))
|
||||
(is (= #{"page y" "page z"}
|
||||
(set (map :block/name (:plugin.property._test_plugin/x7 block')))))
|
||||
(is (= "some content"
|
||||
(:block/title (:plugin.property._test_plugin/x8 block'))))))))
|
||||
|
||||
(deftest remove-block-property-op-rejects-lookup-ref-block-id-test
|
||||
(testing "remove-block-property rejects lookup-ref block ids"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
block (db-test/find-block-by-content @conn "Block")
|
||||
block-uuid (:block/uuid block)]
|
||||
(outliner-property/set-block-property! conn
|
||||
[:block/uuid block-uuid]
|
||||
:logseq.property/order-list-type
|
||||
"number")
|
||||
(is (some? (:logseq.property/order-list-type
|
||||
(d/entity @conn [:block/uuid block-uuid]))))
|
||||
(is (thrown? js/Error
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:remove-block-property [[:block/uuid block-uuid]
|
||||
:logseq.property/order-list-type]]]
|
||||
{}))))))
|
||||
|
||||
(deftest direct-plugin-many-page-property-appends-values-test
|
||||
(testing "direct property operations keep both page values"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
block (db-test/find-block-by-content @conn "Block")
|
||||
block-id (:db/id block)
|
||||
property-id :plugin.property._test_plugin/x7]
|
||||
(outliner-property/upsert-property! conn property-id
|
||||
{:logseq.property/type :page
|
||||
:db/cardinality :db.cardinality/many}
|
||||
{:property-name :x7})
|
||||
(outliner-property/set-block-property! conn block-id property-id "Page y")
|
||||
(outliner-property/set-block-property! conn block-id property-id "Page z")
|
||||
(is (= #{"page y" "page z"}
|
||||
(set (map :block/name
|
||||
(:plugin.property._test_plugin/x7 (d/entity @conn block-id)))))))))
|
||||
|
||||
(deftest apply-template-op-resolves-dynamic-variables-test
|
||||
(testing "apply-template resolves dynamic variables in block title and property values"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "Target Page"}
|
||||
:blocks [{:block/title "target block"}]}
|
||||
{:page {:block/title "Templates"}
|
||||
:blocks [{:block/title "template root"
|
||||
:build/children [{:block/title "page is <% current page %>"}
|
||||
{:block/title "time block"
|
||||
:build/properties {:log-time "<%time%>"}}]}]}]
|
||||
:properties {:log-time {:logseq.property/type :default}}})
|
||||
target-page (ldb/get-page @conn "Target Page")
|
||||
template-root (db-test/find-block-by-content @conn "template root")
|
||||
target-block (db-test/find-block-by-content @conn "target block")
|
||||
template-blocks (->> (ldb/get-block-and-children @conn (:block/uuid template-root)
|
||||
{:include-property-block? true})
|
||||
rest)
|
||||
blocks-to-insert (cons (assoc (into {} (first template-blocks))
|
||||
:db/id (:db/id (first template-blocks))
|
||||
:logseq.property/used-template (:db/id template-root))
|
||||
(map (fn [block]
|
||||
(assoc (into {} block) :db/id (:db/id block)))
|
||||
(rest template-blocks)))
|
||||
_ (outliner-op/apply-ops! conn
|
||||
[[:apply-template [(:block/uuid template-root)
|
||||
(:block/uuid target-block)
|
||||
{:template-blocks blocks-to-insert}]]]
|
||||
{})
|
||||
page-var-block (db-test/find-block-by-content
|
||||
@conn
|
||||
(str "page is " (page-ref/->page-ref (:block/uuid target-page))))
|
||||
time-block (some->> (d/q '[:find [?b ...]
|
||||
:in $ ?title ?page-title
|
||||
:where
|
||||
[?b :block/title ?title]
|
||||
[?b :block/page ?page]
|
||||
[?page :block/title ?page-title]]
|
||||
@conn "time block" "Target Page")
|
||||
first
|
||||
(d/entity @conn))
|
||||
time-value (some (fn [[property-id value]]
|
||||
(when (= "log-time" (name property-id))
|
||||
value))
|
||||
(db-test/readable-properties time-block))]
|
||||
(is (some? page-var-block))
|
||||
(is (string? time-value))
|
||||
(is (not (string/blank? time-value)))
|
||||
(is (not (string/includes? time-value "<%"))))))
|
||||
|
||||
(deftest apply-ops-requires-uuid-block-ids-and-keyword-property-ids-test
|
||||
(testing "ops reject integer eids and accept UUID/keyword identifiers"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "Test"}
|
||||
:blocks [{:block/title "Block"}]}])
|
||||
block (db-test/find-block-by-content @conn "Block")
|
||||
block-id (:db/id block)
|
||||
block-uuid (:block/uuid block)
|
||||
property-kw :plugin.property._test_plugin/normalized-prop]
|
||||
(outliner-property/upsert-property! conn property-kw
|
||||
{:logseq.property/type :checkbox
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:property-name :normalized-prop})
|
||||
(let [property-id (:db/id (d/entity @conn property-kw))]
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:set-block-property [block-uuid property-kw true]]]
|
||||
{})
|
||||
(is (true? (property-kw (d/entity @conn [:block/uuid block-uuid]))))
|
||||
(is (thrown? js/Error
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:set-block-property [block-id property-kw true]]]
|
||||
{})))
|
||||
(is (thrown? js/Error
|
||||
(outliner-op/apply-ops!
|
||||
conn
|
||||
[[:set-block-property [block-uuid property-id true]]]
|
||||
{})))))))
|
||||
206
deps/outliner/test/logseq/outliner/page_test.cljs
vendored
Normal file
206
deps/outliner/test/logseq/outliner/page_test.cljs
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
(ns logseq.outliner.page-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.config :as common-config]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.common.util.page-ref :as page-ref]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.order :as db-order]
|
||||
[logseq.db.frontend.db :as db-db]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.page :as outliner-page]))
|
||||
|
||||
(deftest create-class
|
||||
(let [conn (db-test/create-conn)
|
||||
_ (outliner-page/create! conn "movie" {:class? true})
|
||||
_ (outliner-page/create! conn "Movie" {:class? true})
|
||||
movie-class (ldb/get-case-page @conn "movie")
|
||||
Movie-class (ldb/get-case-page @conn "Movie")]
|
||||
|
||||
(is (ldb/class? movie-class) "Creates a class")
|
||||
(is (ldb/class? Movie-class) "Creates another class with a different case sensitive name")
|
||||
(is (not= movie-class Movie-class) "The two classes are not the same")))
|
||||
|
||||
(deftest create-namespace-pages
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/property1 {:logseq.property/type :default}}
|
||||
:classes {:class1 {}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}}]})]
|
||||
|
||||
(testing "Basic valid workflows"
|
||||
(let [[_ child-uuid] (outliner-page/create! conn "foo/bar/baz" {:split-namespace? true})
|
||||
child-page (d/entity @conn [:block/uuid child-uuid])
|
||||
;; Create a 2nd child page using existing parent pages
|
||||
[_ child-uuid2] (outliner-page/create! conn "foo/bar/baz2" {:split-namespace? true})
|
||||
child-page2 (d/entity @conn [:block/uuid child-uuid2])
|
||||
;; Create a child page for a class
|
||||
[_ child-uuid3] (outliner-page/create! conn "c1/c2" {:split-namespace? true :class? true})
|
||||
child-page3 (d/entity @conn [:block/uuid child-uuid3])
|
||||
library (ldb/get-built-in-page @conn common-config/library-page-name)
|
||||
bar (ldb/get-page @conn "bar")]
|
||||
(is (= ["foo"] (map :block/title (:block/_parent library)))
|
||||
"Namespace (non-class) pages are added to the Library page")
|
||||
(is (= ["baz" "baz2"] (map :block/title (:block/_parent bar)))
|
||||
"Child pages are created under the same parent")
|
||||
(is (= ["foo" "bar"] (map :block/title [(:block/parent (:block/parent child-page))
|
||||
(:block/parent child-page)]))
|
||||
"Child page with new parent has correct parents")
|
||||
(is (= (map :block/uuid (db-db/get-page-parents child-page))
|
||||
(map :block/uuid (db-db/get-page-parents child-page2)))
|
||||
"Child page with existing parents has correct parents")
|
||||
(is (= #{"Root Tag" "c1"} (set (map :block/title (ldb/get-classes-parents [child-page3]))))
|
||||
"Child class with new parent has correct parents")
|
||||
|
||||
(outliner-page/create! conn "foo/class1/baz3" {:split-namespace? true})
|
||||
(is (= #{"Tag" "Page"}
|
||||
(set (d/q '[:find [?tag-title ...]
|
||||
:where
|
||||
[?b :block/title "class1"]
|
||||
[?b :block/tags ?t]
|
||||
[?t :block/title ?tag-title]] @conn)))
|
||||
"Using an existing class page in a multi-parent namespace doesn't allow a page to have a class parent and instead creates a new page")))
|
||||
|
||||
(testing "Child pages with same name and different parents"
|
||||
(let [_ (outliner-page/create! conn "vim/keys" {:split-namespace? true})
|
||||
_ (outliner-page/create! conn "emacs/keys" {:split-namespace? true})]
|
||||
(is (= #{"vim" "emacs"}
|
||||
(->> (d/q '[:find [(pull ?b [{:block/parent [:block/title]}]) ...] :where [?b :block/title "keys"]] @conn)
|
||||
(map #(get-in % [:block/parent :block/title]))
|
||||
set))
|
||||
"Two child pages with same name exist and have different parents")))
|
||||
|
||||
(testing "Invalid workflows"
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Cannot create"
|
||||
(outliner-page/create! conn "class1/page" {:split-namespace? true}))
|
||||
"Page can't have a class parent")
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Cannot create"
|
||||
(outliner-page/create! conn "property1/page" {:split-namespace? true}))
|
||||
"Page can't have a property parent")
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Cannot create"
|
||||
(outliner-page/create! conn "property1/class" {:split-namespace? true :class? true}))
|
||||
"Class can't have a property parent"))))
|
||||
|
||||
(deftest create-page
|
||||
(let [conn (db-test/create-conn)
|
||||
[_ page-uuid] (outliner-page/create! conn "fooz" {})]
|
||||
(is (= "fooz" (:block/title (d/entity @conn [:block/uuid page-uuid])))
|
||||
"Page created correctly")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"can't include \"/"
|
||||
(outliner-page/create! conn "foo/bar" {}))
|
||||
"Page can't have '/'n title")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"can't include \"#\""
|
||||
(outliner-page/create! conn "foo#bar" {}))
|
||||
"Page can't have '#' in title")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"can't include \"#\""
|
||||
(outliner-page/create! conn "#tagstyle" {}))
|
||||
"Page can't have leading '#' in title")))
|
||||
|
||||
(deftest delete-page
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "D1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
d1 (ldb/get-page @conn "D1")
|
||||
b1 (db-test/find-block-by-content @conn "b1")]
|
||||
(ldb/transact! conn [{:db/id (:db/id b1)
|
||||
:block/title (str "b1 " (page-ref/->page-ref (:block/uuid d1)))
|
||||
:block/refs #{(:db/id d1)}}])
|
||||
(is (contains? (set (map :db/id (:block/refs (d/entity @conn (:db/id b1)))))
|
||||
(:db/id d1)))
|
||||
(outliner-page/delete! conn (:block/uuid d1))
|
||||
(let [d1' (d/entity @conn (:db/id d1))
|
||||
b1' (d/entity @conn (:db/id b1))
|
||||
recycle-page (ldb/get-built-in-page @conn "Recycle")]
|
||||
(is (some? d1'))
|
||||
(is (some? b1'))
|
||||
(is (= (:block/uuid recycle-page) (:block/uuid (:block/parent d1'))))
|
||||
(is (integer? (:logseq.property/deleted-at d1')))
|
||||
(is (nil? (:block/raw-title b1')))
|
||||
(is (contains? (set (map :db/id (:block/refs b1')))
|
||||
(:db/id d1)))
|
||||
(is (= (:block/uuid d1') (:block/uuid (:block/page b1')))))))
|
||||
|
||||
(deftest delete-class-page-hard-retracts-page-tree
|
||||
(let [conn (db-test/create-conn-with-blocks {:classes {:Movie {}}})
|
||||
class-page (ldb/get-page @conn "Movie")
|
||||
child-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:block/uuid child-uuid
|
||||
:block/title "class child"
|
||||
:block/page (:db/id class-page)
|
||||
:block/parent (:db/id class-page)
|
||||
:block/order (db-order/gen-key)}])]
|
||||
(outliner-page/delete! conn (:block/uuid class-page))
|
||||
(is (nil? (d/entity @conn [:block/uuid (:block/uuid class-page)])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
|
||||
|
||||
(deftest delete-property-page-hard-retracts-page-tree
|
||||
(let [conn (db-test/create-conn-with-blocks {:properties {:rating {:logseq.property/type :number}}})
|
||||
property-page (d/entity @conn :user.property/rating)
|
||||
child-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:block/uuid child-uuid
|
||||
:block/title "property child"
|
||||
:block/page (:db/id property-page)
|
||||
:block/parent (:db/id property-page)
|
||||
:block/order (db-order/gen-key)}])]
|
||||
(outliner-page/delete! conn (:block/uuid property-page))
|
||||
(is (nil? (d/entity @conn :user.property/rating)))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
|
||||
|
||||
(deftest create-journal
|
||||
(let [conn (db-test/create-conn)
|
||||
[_ page-uuid] (outliner-page/create! conn "Dec 16th, 2024" {})]
|
||||
|
||||
(is (= "Dec 16th, 2024" (:block/title (d/entity @conn [:block/uuid page-uuid])))
|
||||
"Journal created correctly")
|
||||
|
||||
(is (= [:logseq.class/Journal]
|
||||
(->> (d/entity @conn [:block/uuid page-uuid])
|
||||
:block/tags
|
||||
(map #(:db/ident (d/entity @conn (:db/id %))))))
|
||||
"New journal only has Journal tag")))
|
||||
|
||||
(deftest create-journal-keeps-default-block-name-with-custom-title-format
|
||||
(let [conn (db-test/create-conn)
|
||||
_ (d/transact! conn [[:db/add :logseq.class/Journal :logseq.property.journal/title-format "yyyy-MM-dd EEEE"]])
|
||||
[_ page-uuid] (outliner-page/create! conn "Dec 16th, 2024" {})
|
||||
page (d/entity @conn [:block/uuid page-uuid])
|
||||
default-name (-> (:block/journal-day page)
|
||||
(date-time-util/int->journal-title date-time-util/default-journal-title-formatter)
|
||||
common-util/page-name-sanity-lc)]
|
||||
(is (= "2024-12-16 Monday" (:block/title page))
|
||||
"Journal title follows configured formatter")
|
||||
(is (= default-name (:block/name page))
|
||||
"Journal block/name remains the default formatter, independent of title format")))
|
||||
|
||||
(deftest create-slash-formatted-journal-does-not-create-namespace-pages
|
||||
(let [conn (db-test/create-conn)
|
||||
_ (d/transact! conn [[:db/add :logseq.class/Journal :logseq.property.journal/title-format "yyyy/MM/dd"]])
|
||||
[_ page-uuid] (outliner-page/create! conn "May 18th, 2026" {:split-namespace? true
|
||||
:journal? true})
|
||||
page (d/entity @conn [:block/uuid page-uuid])]
|
||||
(is (= "2026/05/18" (:block/title page))
|
||||
"Journal title follows slash title format")
|
||||
(is (= (common-uuid/gen-uuid :journal-page-uuid 20260518) (:block/uuid page))
|
||||
"Journal page has the standard journal uuid")
|
||||
(is (nil? (ldb/get-page @conn "2026"))
|
||||
"Journal title is not split into a year namespace page")
|
||||
(is (nil? (ldb/get-page @conn "05"))
|
||||
"Journal title is not split into a month namespace page")
|
||||
(is (nil? (ldb/get-page @conn "18"))
|
||||
"Journal title is not split into a day namespace page")))
|
||||
47
deps/outliner/test/logseq/outliner/pipeline_test.cljs
vendored
Normal file
47
deps/outliner/test/logseq/outliner/pipeline_test.cljs
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
(ns logseq.outliner.pipeline-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.util.page-ref :as page-ref]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.pipeline :as outliner-pipeline]))
|
||||
|
||||
(deftest block-content-refs
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"} :blocks [{:block/title "b1"}]}])
|
||||
block (db-test/find-block-by-content @conn "b1")]
|
||||
(assert block)
|
||||
(is (= [(:db/id block)]
|
||||
(outliner-pipeline/block-content-refs @conn
|
||||
{:block/title (str "ref to " (page-ref/->page-ref (:block/uuid block)))})))))
|
||||
|
||||
(deftest db-rebuild-block-refs-for-query-block
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "Todo query"
|
||||
:build/tags [:logseq.class/Query]
|
||||
:build/properties
|
||||
{:logseq.property/query
|
||||
{:build/property-value :block
|
||||
:block/title "(task Todo)"}}}]}]})
|
||||
block (db-test/find-block-by-content @conn "Todo query")
|
||||
refs (set (outliner-pipeline/db-rebuild-block-refs @conn block))
|
||||
query-property-id (:db/id (d/entity @conn :logseq.property/query))
|
||||
query-class-id (:db/id (d/entity @conn :logseq.class/Query))]
|
||||
(is (some? query-property-id)
|
||||
"Sanity: :logseq.property/query entity exists")
|
||||
(is (contains? refs query-class-id)
|
||||
"#Query class tag is included in :block/refs")
|
||||
(is (not (contains? refs query-property-id))
|
||||
"#Query block does not reference logseq.property/query through :block/refs")))
|
||||
|
||||
(deftest db-rebuild-block-refs-removes-recursive-self-ref
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "self"}]}])
|
||||
block (db-test/find-block-by-content @conn "self")
|
||||
block' (assoc (d/pull @conn '[:db/id :block/uuid :block/title] (:db/id block))
|
||||
:block/title
|
||||
(str "self " (page-ref/->page-ref (:block/uuid block))))]
|
||||
(is (empty? (outliner-pipeline/db-rebuild-block-refs @conn block'))
|
||||
"A block should not rebuild a recursive ref to itself")))
|
||||
694
deps/outliner/test/logseq/outliner/property_test.cljs
vendored
Normal file
694
deps/outliner/test/logseq/outliner/property_test.cljs
vendored
Normal file
@@ -0,0 +1,694 @@
|
||||
(ns logseq.outliner.property-test
|
||||
(:require [cljs.test :refer [deftest is testing are]]
|
||||
[datascript.core :as d]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.property :as outliner-property]))
|
||||
|
||||
(deftest upsert-property!
|
||||
(testing "Creates a property"
|
||||
(let [conn (db-test/create-conn-with-blocks [])
|
||||
_ (outliner-property/upsert-property! conn nil {:logseq.property/type :number} {:property-name "num"})]
|
||||
(is (= :number
|
||||
(:logseq.property/type (d/entity @conn :user.property/num)))
|
||||
"Creates property with property-name")))
|
||||
|
||||
(testing "Updates a property"
|
||||
(let [conn (db-test/create-conn-with-blocks {:properties {:num {:logseq.property/type :number}}})
|
||||
old-updated-at (:block/updated-at (d/entity @conn :user.property/num))]
|
||||
|
||||
(testing "and change its cardinality"
|
||||
(outliner-property/upsert-property! conn :user.property/num {:db/cardinality :many} {})
|
||||
(is (db-property/many? (d/entity @conn :user.property/num)))
|
||||
(is (> (:block/updated-at (d/entity @conn :user.property/num))
|
||||
old-updated-at)))
|
||||
|
||||
(testing "and change its type from a ref to a non-ref type"
|
||||
(outliner-property/upsert-property! conn :user.property/num {:logseq.property/type :checkbox} {})
|
||||
(is (= :checkbox (:logseq.property/type (d/entity @conn :user.property/num))))
|
||||
(is (= nil (:db/valueType (d/entity @conn :user.property/num)))))))
|
||||
|
||||
(testing "Multiple properties that generate the same initial :db/ident"
|
||||
(let [conn (db-test/create-conn-with-blocks [])]
|
||||
(outliner-property/upsert-property! conn nil {:logseq.property/type :default} {:property-name "p1"})
|
||||
(outliner-property/upsert-property! conn nil {} {:property-name "p1"})
|
||||
(outliner-property/upsert-property! conn nil {} {:property-name "p1"})
|
||||
|
||||
(is (= {:block/name "p1" :block/title "p1" :logseq.property/type :default}
|
||||
(select-keys (d/entity @conn :user.property/p1) [:block/name :block/title :logseq.property/type]))
|
||||
"Existing db/ident does not get modified")
|
||||
(is (= "p1"
|
||||
(:block/title (d/entity @conn :user.property/p1-1)))
|
||||
"2nd property gets unique ident")
|
||||
(is (= "p1"
|
||||
(:block/title (d/entity @conn :user.property/p1-2)))
|
||||
"3rd property gets unique ident"))))
|
||||
|
||||
(deftest upsert-property-rejects-type-change-with-existing-data
|
||||
(testing "Changing type is rejected when property has values"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:status "active"}}]}])]
|
||||
(is (thrown-with-msg?
|
||||
js/Error #"Disallowed type change with existing data"
|
||||
(outliner-property/upsert-property! conn :user.property/status {:logseq.property/type :number} {})))))
|
||||
|
||||
(testing "Changing type is allowed when property has no values"
|
||||
(let [conn (db-test/create-conn-with-blocks {:properties {:empty-prop {:logseq.property/type :default}}})]
|
||||
(outliner-property/upsert-property! conn :user.property/empty-prop {:logseq.property/type :number} {})
|
||||
(is (= :number (:logseq.property/type (d/entity @conn :user.property/empty-prop)))))))
|
||||
|
||||
(deftest convert-property-input-string
|
||||
(testing "Convert property input string according to its schema type"
|
||||
(let [test-uuid (random-uuid)]
|
||||
(are [x y]
|
||||
(= (let [[schema-type value] x]
|
||||
(outliner-property/convert-property-input-string nil {:logseq.property/type schema-type} value)) y)
|
||||
[:number "1"] 1
|
||||
[:number "1.2"] 1.2
|
||||
[:url test-uuid] test-uuid
|
||||
[:date test-uuid] test-uuid
|
||||
[:any test-uuid] test-uuid
|
||||
[nil test-uuid] test-uuid))))
|
||||
|
||||
(deftest create-property-text-block!
|
||||
(testing "Create a new :default property value"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:default "foo"}}
|
||||
{:block/title "b2"}]}])
|
||||
block (db-test/find-block-by-content @conn "b2")
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/default "" {})
|
||||
new-property-value (:user.property/default (db-test/find-block-by-content @conn "b2"))]
|
||||
|
||||
(is (some? (:db/id new-property-value)) "New property value created")
|
||||
(is (= "" (db-property/property-value-content new-property-value))
|
||||
"Property value has correct content")
|
||||
(is (= :user.property/default
|
||||
(get-in (d/entity @conn (:db/id new-property-value)) [:logseq.property/created-from-property :db/ident]))
|
||||
"Has correct created-from-property")))
|
||||
|
||||
(testing "Create cases for a new :one :number property value"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num 2}}
|
||||
{:block/title "b2"}]}])
|
||||
block (db-test/find-block-by-content @conn "b2")
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/num "3" {})
|
||||
new-property-value (:user.property/num (db-test/find-block-by-content @conn "b2"))]
|
||||
|
||||
(is (some? (:db/id new-property-value)) "New property value created")
|
||||
(is (= 3 (db-property/property-value-content new-property-value))
|
||||
"Property value has correct content")
|
||||
(is (= :user.property/num
|
||||
(get-in (d/entity @conn (:db/id new-property-value)) [:logseq.property/created-from-property :db/ident]))
|
||||
"Has correct created-from-property")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't convert"
|
||||
(outliner-property/create-property-text-block! conn (:db/id block) :user.property/num "Not a number" {}))
|
||||
"Wrong value isn't transacted")))
|
||||
|
||||
(testing "Create new :many :number property values"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num-many #{2}}}
|
||||
{:block/title "b2"}]}])
|
||||
block (db-test/find-block-by-content @conn "b2")
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/num-many "3" {})
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/num-many "4" {})
|
||||
_ (outliner-property/create-property-text-block! conn (:db/id block) :user.property/num-many "5" {})
|
||||
new-property-values (:user.property/num-many (db-test/find-block-by-content @conn "b2"))]
|
||||
|
||||
(is (seq new-property-values) "New property values created")
|
||||
(is (= #{3 4 5} (set (map db-property/property-value-content new-property-values)))
|
||||
"Property value has correct content"))))
|
||||
|
||||
(deftest set-block-property-basic-cases
|
||||
(testing "Set a :number value with existing value"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num 2}}
|
||||
{:block/title "b2"}]}])
|
||||
property-value (:user.property/num (db-test/find-block-by-content @conn "b1"))
|
||||
_ (assert (:db/id property-value))
|
||||
block-uuid (:block/uuid (db-test/find-block-by-content @conn "b2"))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/set-block-property! conn [:block/uuid block-uuid] :user.property/num (:db/id property-value))]
|
||||
(is (= (:db/id property-value)
|
||||
(:db/id (:user.property/num (db-test/find-block-by-content @conn "b2")))))
|
||||
(let [empty-placeholder-id (:db/id (d/entity @conn :logseq.property/empty-placeholder))]
|
||||
(outliner-property/set-block-property! conn [:block/uuid block-uuid] :user.property/num empty-placeholder-id)
|
||||
(is (= empty-placeholder-id
|
||||
(:logseq.property/value (:user.property/num (d/entity @conn [:block/uuid block-uuid]))))))))
|
||||
|
||||
(testing "Update a :number value with existing value"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num 2}}
|
||||
{:block/title "b2" :build/properties {:num 3}}]}])
|
||||
property-value (:user.property/num (db-test/find-block-by-content @conn "b1"))
|
||||
_ (assert (:db/id property-value))
|
||||
block-uuid (:block/uuid (db-test/find-block-by-content @conn "b2"))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/set-block-property! conn [:block/uuid block-uuid] :user.property/num (:db/id property-value))]
|
||||
(is (= (:db/id property-value)
|
||||
(:db/id (:user.property/num (db-test/find-block-by-content @conn "b2"))))))))
|
||||
|
||||
(deftest set-block-property-with-non-ref-values
|
||||
(testing "Setting :default with same property value reuses existing entity"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:logseq.property/order-list-type "number"}}
|
||||
{:block/title "b2"}]}])
|
||||
property-value (:logseq.property/order-list-type (db-test/find-block-by-content @conn "b1"))
|
||||
block-uuid (:block/uuid (db-test/find-block-by-content @conn "b2"))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/set-block-property! conn [:block/uuid block-uuid] :logseq.property/order-list-type "number")]
|
||||
(is (some? (:db/id (:logseq.property/order-list-type (db-test/find-block-by-content @conn "b2"))))
|
||||
"New block has property set")
|
||||
(is (= (:db/id property-value)
|
||||
(:db/id (:logseq.property/order-list-type (db-test/find-block-by-content @conn "b2")))))))
|
||||
|
||||
(testing "Setting :checkbox with same property value reuses existing entity"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:checkbox true}}
|
||||
{:block/title "b2"}]}])
|
||||
property-value (:user.property/checkbox (db-test/find-block-by-content @conn "b1"))
|
||||
block-uuid (:block/uuid (db-test/find-block-by-content @conn "b2"))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/set-block-property! conn [:block/uuid block-uuid] :user.property/checkbox true)]
|
||||
(is (true? (:user.property/checkbox (db-test/find-block-by-content @conn "b2")))
|
||||
"New block has property set")
|
||||
(is (= property-value (:user.property/checkbox (db-test/find-block-by-content @conn "b2")))))))
|
||||
|
||||
(deftest remove-block-property!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:default "foo"}}]}])
|
||||
block (db-test/find-block-by-content @conn "b1")
|
||||
_ (assert (:user.property/default block))
|
||||
;; Use same args as outliner.op
|
||||
_ (outliner-property/remove-block-property! conn [:block/uuid (:block/uuid block)] :user.property/default)
|
||||
updated-block (db-test/find-block-by-content @conn "b1")]
|
||||
(is (some? updated-block))
|
||||
(is (nil? (:user.property/default updated-block)) "Block property is deleted")))
|
||||
|
||||
(deftest batch-set-property!
|
||||
(testing "Set built-in property values for multiple blocks"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "item 1"}
|
||||
{:block/title "item 2"}]}])
|
||||
block-ids (map #(-> (db-test/find-block-by-content @conn %) :block/uuid) ["item 1" "item 2"])
|
||||
_ (outliner-property/batch-set-property! conn block-ids :logseq.property/order-list-type "number")
|
||||
updated-blocks (map #(db-test/find-block-by-content @conn %) ["item 1" "item 2"])]
|
||||
(is (= ["number" "number"]
|
||||
(map #(db-property/property-value-content (:logseq.property/order-list-type %))
|
||||
updated-blocks))
|
||||
"Property values are batch set")))
|
||||
|
||||
(testing "Set custom default-many property values from string vector"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/reproducible-steps {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/many
|
||||
:logseq.property/public? true}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}]})
|
||||
block-id (:block/uuid (db-test/find-block-by-content @conn "target"))]
|
||||
(outliner-property/batch-set-property! conn [block-id] :user.property/reproducible-steps ["Step 1" "Step 2" "Step 3"])
|
||||
(is (= #{"Step 1" "Step 2" "Step 3"}
|
||||
(->> (:user.property/reproducible-steps (d/entity @conn [:block/uuid block-id]))
|
||||
(map db-property/property-value-content)
|
||||
set))
|
||||
"String vector values are persisted as many property values")))
|
||||
|
||||
(testing "Set custom default-many property values from id vector"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/reproducible-steps {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/many
|
||||
:logseq.property/public? true}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "source"
|
||||
:build/properties {:user.property/reproducible-steps #{"Step 1" "Step 2" "Step 3"}}}
|
||||
{:block/title "target"}]}]})
|
||||
source-values (->> (:user.property/reproducible-steps (db-test/find-block-by-content @conn "source"))
|
||||
(map :db/id)
|
||||
vec)
|
||||
target-id (:block/uuid (db-test/find-block-by-content @conn "target"))]
|
||||
(outliner-property/batch-set-property! conn [target-id] :user.property/reproducible-steps source-values)
|
||||
(is (= #{"Step 1" "Step 2" "Step 3"}
|
||||
(->> (:user.property/reproducible-steps (d/entity @conn [:block/uuid target-id]))
|
||||
(map db-property/property-value-content)
|
||||
set))
|
||||
"Id vector values are persisted as many property values")))
|
||||
|
||||
(testing "Invalid many values throw and don't partially persist"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/reproducible-steps {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/many
|
||||
:logseq.property/public? true}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}]})
|
||||
block-id (:block/uuid (db-test/find-block-by-content @conn "target"))]
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Schema validation failed"
|
||||
(outliner-property/batch-set-property! conn [block-id] :user.property/reproducible-steps [999999])))
|
||||
(is (nil? (:user.property/reproducible-steps (d/entity @conn [:block/uuid block-id])))
|
||||
"No partial values are persisted on failure"))))
|
||||
|
||||
(deftest status-property-setting-classes
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:Cat {}
|
||||
:Project {:build/class-properties [:logseq.property/status]}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title ""}
|
||||
{:block/title "cat task" :build/tags [:Cat]}
|
||||
{:block/title "project task" :build/tags [:Project]}]}]})
|
||||
page1 (:block/uuid (db-test/find-page-by-title @conn "page1"))
|
||||
[empty-task cat-task project]
|
||||
(map #(:block/uuid (db-test/find-block-by-content @conn %)) ["" "cat task" "project task"])]
|
||||
|
||||
(outliner-property/batch-set-property! conn [empty-task] :logseq.property/status :logseq.property/status.doing)
|
||||
(is (= [:logseq.class/Task]
|
||||
(mapv :db/ident (:block/tags (d/entity @conn [:block/uuid empty-task]))))
|
||||
"Adds Task to block when it is not tagged")
|
||||
|
||||
(outliner-property/batch-set-property! conn [page1] :logseq.property/status :logseq.property/status.doing)
|
||||
(is (= #{:logseq.class/Task :logseq.class/Page}
|
||||
(set (map :db/ident (:block/tags (d/entity @conn [:block/uuid page1])))))
|
||||
"Adds Task to page without tag")
|
||||
|
||||
(outliner-property/batch-set-property! conn [cat-task] :logseq.property/status :logseq.property/status.doing)
|
||||
(is (= #{:logseq.class/Task :user.class/Cat}
|
||||
(set (map :db/ident (:block/tags (d/entity @conn [:block/uuid cat-task])))))
|
||||
"Adds Task to tagged block when its classes do not provide status")
|
||||
|
||||
(outliner-property/batch-set-property! conn [project] :logseq.property/status :logseq.property/status.doing)
|
||||
(is (= [:user.class/Project]
|
||||
(mapv :db/ident (:block/tags (d/entity @conn [:block/uuid project]))))
|
||||
"Doesn't add Task to block when its class provides status")))
|
||||
|
||||
(deftest batch-set-property-rejects-private-built-in-entity
|
||||
(let [conn (db-test/create-conn)
|
||||
placeholder (d/entity @conn :logseq.property/empty-placeholder)
|
||||
prop (d/entity @conn :logseq.property/description)]
|
||||
(is (thrown-with-msg? js/Error #"Built-in.*can't be modified"
|
||||
(db-test/silence-stderr
|
||||
(outliner-property/batch-set-property! conn [(:db/id placeholder)] (:db/ident prop) "hacked"))))))
|
||||
|
||||
(deftest batch-remove-property!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:C1 {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "item 1" :build/properties {:logseq.property/order-list-type "number"}}
|
||||
{:block/title "item 2" :build/properties {:logseq.property/order-list-type "number"}}]}]})
|
||||
block-ids (map #(-> (db-test/find-block-by-content @conn %) :block/uuid) ["item 1" "item 2"])
|
||||
_ (outliner-property/batch-remove-property! conn block-ids :logseq.property/order-list-type)
|
||||
updated-blocks (map #(db-test/find-block-by-content @conn %) ["item 1" "item 2"])]
|
||||
(is (= [nil nil]
|
||||
(map :logseq.property/order-list-type updated-blocks))
|
||||
"Property values are batch removed")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't remove private"
|
||||
(outliner-property/batch-remove-property! conn [(:db/id (db-test/find-page-by-title @conn "page1"))] :block/tags)))
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't remove required"
|
||||
(outliner-property/batch-remove-property! conn [(:db/id (d/entity @conn :user.class/C1))] :logseq.property.class/extends)))))
|
||||
|
||||
(deftest batch-remove-property-rejects-private-built-in-entity
|
||||
(let [conn (db-test/create-conn)
|
||||
placeholder (d/entity @conn :logseq.property/empty-placeholder)
|
||||
prop (d/entity @conn :logseq.property/description)]
|
||||
(is (thrown-with-msg? js/Error #"Built-in.*can't be modified"
|
||||
(db-test/silence-stderr
|
||||
(outliner-property/batch-remove-property! conn [(:db/id placeholder)] (:db/ident prop)))))))
|
||||
|
||||
(deftest add-existing-values-to-closed-values!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :build/properties {:num 1}}
|
||||
{:block/title "b2" :build/properties {:num 2}}]}])
|
||||
values (map (fn [d] (:block/uuid (d/entity @conn (:v d)))) (d/datoms @conn :avet :user.property/num))
|
||||
_ (outliner-property/add-existing-values-to-closed-values! conn :user.property/num values)]
|
||||
(is (= [1 2]
|
||||
(map db-property/closed-value-content (:block/_closed-value-property (d/entity @conn :user.property/num)))))))
|
||||
|
||||
(deftest add-existing-generated-value-to-closed-values-reparents-to-property
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:user.property/reproducible-steps {:logseq.property/type :default
|
||||
:db/cardinality :db.cardinality/many
|
||||
:logseq.property/public? true}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}]})
|
||||
property (d/entity @conn :user.property/reproducible-steps)
|
||||
block (db-test/find-block-by-content @conn "target")
|
||||
block-uuid (:block/uuid block)]
|
||||
(outliner-property/batch-set-property! conn [block-uuid] :user.property/reproducible-steps ["Step 1"])
|
||||
(let [generated-value (first (:user.property/reproducible-steps (d/entity @conn [:block/uuid block-uuid])))
|
||||
value-uuid (:block/uuid generated-value)]
|
||||
(is (= (:db/id block) (:db/id (:block/parent generated-value))))
|
||||
|
||||
(outliner-property/add-existing-values-to-closed-values! conn :user.property/reproducible-steps [value-uuid])
|
||||
(let [closed-value (d/entity @conn [:block/uuid value-uuid])]
|
||||
(is (= #{(:db/id property)}
|
||||
(set (map :db/id (:block/closed-value-property closed-value)))))
|
||||
(is (= (:db/id property) (:db/id (:block/parent closed-value))))
|
||||
(is (= (:db/id property) (:db/id (:block/page closed-value))))
|
||||
(is (= "Step 1" (db-property/closed-value-content closed-value)))
|
||||
|
||||
(outliner-core/delete-blocks! conn [(d/entity @conn [:block/uuid block-uuid])] {})
|
||||
(is (some? (d/entity @conn [:block/uuid value-uuid])))))))
|
||||
|
||||
(deftest upsert-closed-value!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:num {:build/closed-values [{:uuid (random-uuid) :value 2}]
|
||||
:logseq.property/type :number}}})]
|
||||
|
||||
(testing "Add non-number choice shouldn't work"
|
||||
(is
|
||||
(thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't convert"
|
||||
(outliner-property/upsert-closed-value! conn :user.property/num {:value "not a number"}))))
|
||||
|
||||
(testing "Can't add existing choice"
|
||||
(is
|
||||
(thrown-with-msg?
|
||||
js/Error
|
||||
#"Closed value choice already exists"
|
||||
(outliner-property/upsert-closed-value! conn :user.property/num {:value 2}))))
|
||||
|
||||
(testing "Add choice successfully"
|
||||
(let [_ (outliner-property/upsert-closed-value! conn :user.property/num {:value 3})
|
||||
b (first (d/q '[:find [(pull ?b [*]) ...] :where [?b :logseq.property/value 3]] @conn))]
|
||||
(is (ldb/closed-value? (d/entity @conn (:db/id b))))
|
||||
(is (= [2 3]
|
||||
(map db-property/closed-value-content (:block/_closed-value-property (d/entity @conn :user.property/num)))))))
|
||||
|
||||
(testing "Update choice successfully"
|
||||
(let [b (first (d/q '[:find [(pull ?b [*]) ...] :where [?b :logseq.property/value 2]] @conn))
|
||||
_ (outliner-property/upsert-closed-value! conn :user.property/num {:id (:block/uuid b)
|
||||
:value 4
|
||||
:description "choice 4"})
|
||||
updated-b (d/entity @conn [:block/uuid (:block/uuid b)])]
|
||||
(is (= 4 (db-property/closed-value-content updated-b)))
|
||||
(is (= "choice 4" (db-property/property-value-content (:logseq.property/description updated-b))))))))
|
||||
|
||||
(deftest delete-closed-value!
|
||||
(let [closed-value-uuid (random-uuid)
|
||||
used-closed-value-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:properties {:default {:build/closed-values [{:uuid closed-value-uuid :value "foo"}
|
||||
{:uuid used-closed-value-uuid :value "bar"}]
|
||||
:logseq.property/type :default}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1" :user.property/default [:block/uuid used-closed-value-uuid]}]}]})
|
||||
_ (assert (:user.property/default (db-test/find-block-by-content @conn "b1")))
|
||||
property-id (:db/id (d/entity @conn :user.property/default))
|
||||
closed-value-id (:db/id (d/entity @conn [:block/uuid closed-value-uuid]))
|
||||
child-uuid (random-uuid)
|
||||
_ (d/transact! conn [{:block/uuid child-uuid
|
||||
:block/title "closed value child"
|
||||
:block/page property-id
|
||||
:block/parent closed-value-id
|
||||
:block/order "a0"}])
|
||||
_ (outliner-property/delete-closed-value! conn property-id [:block/uuid closed-value-uuid])]
|
||||
(is (nil? (d/entity @conn [:block/uuid closed-value-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))
|
||||
(is (= [used-closed-value-uuid]
|
||||
(mapv :block/uuid (:block/_closed-value-property (d/entity @conn :user.property/default)))))))
|
||||
|
||||
(deftest class-add-property!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:c1 {}}
|
||||
:properties {:p1 {:logseq.property/type :default}
|
||||
:p2 {:logseq.property/type :default}}})
|
||||
_ (outliner-property/class-add-property! conn :user.class/c1 :user.property/p1)
|
||||
_ (outliner-property/class-add-property! conn :user.class/c1 :user.property/p2)]
|
||||
(is (= [:user.property/p1 :user.property/p2]
|
||||
(map :db/ident (:logseq.property.class/properties (d/entity @conn :user.class/c1)))))))
|
||||
|
||||
(deftest class-remove-property!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:c1 {:build/class-properties [:p1 :p2]}}})
|
||||
_ (outliner-property/class-remove-property! conn :user.class/c1 :user.property/p1)]
|
||||
(is (= [:user.property/p2]
|
||||
(map :db/ident (:logseq.property.class/properties (d/entity @conn :user.class/c1)))))))
|
||||
|
||||
(deftest get-block-classes-properties
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:c1 {:build/class-properties [:p1]}
|
||||
:c2 {:build/class-properties [:p2 :p3]}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "p1"}
|
||||
:blocks [{:block/title "o1"
|
||||
:build/tags [:c1 :c2]}]}]})
|
||||
block (db-test/find-block-by-content @conn "o1")]
|
||||
(is (= [:user.property/p1 :user.property/p2 :user.property/p3]
|
||||
(map :db/ident (:classes-properties (outliner-property/get-block-classes-properties @conn (:db/id block))))))))
|
||||
|
||||
(deftest property-with-other-position-default-bottom-rules
|
||||
(testing "explicit non-properties ui-position remains other-position"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :number
|
||||
:logseq.property/ui-position :block-left}))))
|
||||
|
||||
(testing "number property without explicit ui-position defaults to bottom position"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :number}))))
|
||||
|
||||
(testing "default property without closed values stays in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :default
|
||||
:property/closed-values []}))))
|
||||
|
||||
(testing "default property with closed values is positioned in bottom row"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :default
|
||||
:property/closed-values [{:db/id 1}]}))))
|
||||
|
||||
(testing "url property without explicit ui-position stays in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :url}))))
|
||||
|
||||
(testing "explicit left/right ui-position remains in positioned rows"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :url
|
||||
:logseq.property/ui-position :block-left}))))
|
||||
|
||||
(testing "explicit properties ui-position keeps property in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :number
|
||||
:logseq.property/ui-position :properties}))))
|
||||
|
||||
(testing "many node property without explicit ui-position defaults to bottom position"
|
||||
(is (true?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :user.property/p1
|
||||
:logseq.property/type :node
|
||||
:db/cardinality :db.cardinality/many}))))
|
||||
|
||||
(testing "bidirectional config property stays in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :logseq.property.class/enable-bidirectional?
|
||||
:logseq.property/type :checkbox}))))
|
||||
|
||||
(testing "tag properties and schema-related properties stay in normal property rows"
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :block/tags
|
||||
:logseq.property/type :class})))
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :logseq.property.class/properties
|
||||
:logseq.property/type :property})))
|
||||
(is (false?
|
||||
(outliner-property/property-with-other-position?
|
||||
nil
|
||||
{}
|
||||
{:db/ident :logseq.property/public?
|
||||
:logseq.property/type :checkbox}))))
|
||||
|
||||
(testing "a schema property is part of the shared schema set"
|
||||
(is (contains? db-property/schema-properties :logseq.property/public?))))
|
||||
|
||||
(deftest get-block-positioned-properties-filters-non-public
|
||||
(let [conn (db-test/create-conn-with-blocks [])
|
||||
journal-class (d/entity @conn :logseq.class/Journal)
|
||||
positioned-properties (outliner-property/get-block-positioned-properties @conn (:db/id journal-class) :block-below)]
|
||||
(is (every? #(not (false? (:logseq.property/public? %))) positioned-properties))
|
||||
(is (not-any? #(= :logseq.property.journal/title-format (:db/ident %))
|
||||
positioned-properties))))
|
||||
|
||||
(deftest get-block-positioned-properties-keeps-empty-inherited-tag-properties
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:authors {:logseq.property/type :node
|
||||
:db/cardinality :db.cardinality/many}}
|
||||
:classes {:Paper {:build/class-properties [:authors]}}
|
||||
:pages-and-blocks [{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "paper1"
|
||||
:build/tags [:Paper]}]}]})
|
||||
paper1 (db-test/find-block-by-content @conn "paper1")
|
||||
positioned-properties (outliner-property/get-block-positioned-properties @conn (:db/id paper1) :block-below)
|
||||
positioned-idents (set (map :db/ident positioned-properties))]
|
||||
(is (contains? positioned-idents :user.property/authors))))
|
||||
|
||||
(deftest extends-cycle
|
||||
(testing "Fail when creating a cycle of extends"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:Class1 {}
|
||||
:Class2 {}
|
||||
:Class3 {}}})
|
||||
db @conn
|
||||
class1 (d/entity db :user.class/Class1)
|
||||
class2 (d/entity db :user.class/Class2)
|
||||
class3 (d/entity db :user.class/Class3)]
|
||||
(outliner-property/set-block-property! conn (:db/id class1) :logseq.property.class/extends (:db/id class2))
|
||||
(outliner-property/set-block-property! conn (:db/id class2) :logseq.property.class/extends (:db/id class3))
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Extends cycle"
|
||||
(outliner-property/set-block-property! conn (:db/id class3) :logseq.property.class/extends (:db/id class1)))
|
||||
"Extends cycle"))))
|
||||
|
||||
(deftest extends-redundant-direct-parent-cleanup
|
||||
(testing "Clean redundant direct parents from descendants when a class gets a new parent"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:A {}
|
||||
:B {}
|
||||
:C {:build/class-extends [:B]}
|
||||
:D {:build/class-extends [:A :C]}}})
|
||||
b (d/entity @conn :user.class/B)]
|
||||
(outliner-property/set-block-property! conn
|
||||
(:db/id b)
|
||||
:logseq.property.class/extends
|
||||
(:db/id (d/entity @conn :user.class/A)))
|
||||
(is (= [:user.class/C]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/D))))
|
||||
"D keeps only the nearest direct parent"))))
|
||||
|
||||
(deftest extends-redundant-cleanup-with-lookup-ref-parent
|
||||
(testing "Treat lookup refs as a single parent reference"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:A {}
|
||||
:B {}
|
||||
:C {:build/class-extends [:B]}
|
||||
:D {:build/class-extends [:A :C]}}})]
|
||||
(outliner-property/set-block-property! conn
|
||||
(:db/id (d/entity @conn :user.class/B))
|
||||
:logseq.property.class/extends
|
||||
[:db/ident :user.class/A])
|
||||
(is (= [:user.class/A]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/B))))
|
||||
"B uses the lookup ref as one parent")
|
||||
(is (= [:user.class/C]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/D))))
|
||||
"D removes the direct parent inherited through C"))))
|
||||
|
||||
(deftest extends-redundant-cleanup-with-keyword-vector-parents
|
||||
(testing "Treat a vector of class idents as multiple parents"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:A {}
|
||||
:B {}
|
||||
:C {:build/class-extends [:A]}
|
||||
:D {:build/class-extends [:A :B]}}})]
|
||||
(outliner-property/batch-set-property! conn
|
||||
[(:db/id (d/entity @conn :user.class/B))]
|
||||
:logseq.property.class/extends
|
||||
[:user.class/A :user.class/C])
|
||||
(is (= [:user.class/C]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/B))))
|
||||
"B keeps only the nearest parent")
|
||||
(is (= [:user.class/B]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/D))))
|
||||
"D removes the direct parent inherited through B"))))
|
||||
|
||||
(deftest extends-redundant-direct-parent-cleanup-for-root-reset
|
||||
(testing "Clean descendant Root parents when an ancestor is reset to Root"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:B {}
|
||||
:C {:build/class-extends [:B]}
|
||||
:D {:build/class-extends [:logseq.class/Root :C]}}})]
|
||||
(outliner-property/set-block-property! conn
|
||||
(:db/id (d/entity @conn :user.class/B))
|
||||
:logseq.property.class/extends
|
||||
:logseq.class/Root)
|
||||
(is (= [:user.class/C]
|
||||
(:logseq.property.class/extends
|
||||
(db-test/readable-properties (d/entity @conn :user.class/D))))
|
||||
"D removes the direct Root parent inherited through C"))))
|
||||
|
||||
(deftest delete-property-value!
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:C1 {}
|
||||
:C2 {}
|
||||
:C3 {:build/class-extends [:C1 :C2]}}})]
|
||||
(outliner-property/delete-property-value! conn :user.class/C3 :logseq.property.class/extends
|
||||
(:db/id (d/entity @conn :user.class/C2)))
|
||||
(is (= [:user.class/C1]
|
||||
(:logseq.property.class/extends (db-test/readable-properties (d/entity @conn :user.class/C3))))
|
||||
"Specific property value is deleted")
|
||||
|
||||
(outliner-property/delete-property-value! conn :user.class/C3 :logseq.property.class/extends
|
||||
(:db/id (d/entity @conn :user.class/C1)))
|
||||
(is (= [:logseq.class/Root]
|
||||
(:logseq.property.class/extends (db-test/readable-properties (d/entity @conn :user.class/C3))))
|
||||
"Extends property is restored back to Root")))
|
||||
214
deps/outliner/test/logseq/outliner/recycle_test.cljs
vendored
Normal file
214
deps/outliner/test/logseq/outliner/recycle_test.cljs
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
(ns logseq.outliner.recycle-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[datascript.core :as d]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.op :as outliner-op]
|
||||
[logseq.outliner.recycle :as recycle]))
|
||||
|
||||
(deftest restore-recycled-page-removes-recycle-parent
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
page (ldb/get-page @conn "page1")]
|
||||
(recycle/recycle-page-tx-data @conn page {})
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
|
||||
(recycle/restore! conn (:block/uuid page))
|
||||
(let [page' (ldb/get-page @conn "page1")]
|
||||
(is (nil? (:block/parent page')))
|
||||
(is (nil? (:logseq.property/deleted-at page')))
|
||||
(is (nil? (:logseq.property.recycle/original-parent page'))))))
|
||||
|
||||
(deftest apply-ops-restore-recycled-page-removes-recycle-parent
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
page-uuid (:block/uuid page)]
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid page-uuid]))))
|
||||
(outliner-op/apply-ops! conn [[:restore-recycled [page-uuid]]] {})
|
||||
(let [page' (ldb/get-page @conn "page1")]
|
||||
(is (nil? (:block/parent page')))
|
||||
(is (nil? (:logseq.property/deleted-at page')))
|
||||
(is (nil? (:logseq.property.recycle/original-parent page'))))))
|
||||
|
||||
(deftest permanently-delete-recycled-page-removes-page-and-descendants
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
block (db-test/find-block-by-content @conn "b1")
|
||||
page-uuid (:block/uuid page)
|
||||
block-uuid (:block/uuid block)]
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid page-uuid]))))
|
||||
(is (true? (recycle/permanently-delete! conn page-uuid)))
|
||||
(is (nil? (d/entity @conn [:block/uuid page-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid block-uuid])))))
|
||||
|
||||
(deftest permanently-delete-recycled-page-removes-blocks-parented-by-page
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}}
|
||||
{:page {:block/title "page2"}}])
|
||||
page1 (ldb/get-page @conn "page1")
|
||||
page2 (ldb/get-page @conn "page2")
|
||||
block-uuid (random-uuid)
|
||||
now (common-util/time-ms)]
|
||||
(d/transact! conn [{:block/uuid block-uuid
|
||||
:block/title "parented by page1"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/parent (:db/id page1)
|
||||
:block/page (:db/id page2)
|
||||
:block/order "a0"}])
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page1 {}) {:outliner-op :delete-page})
|
||||
(is (true? (ldb/recycled? (d/entity @conn (:db/id page1)))))
|
||||
(is (true? (recycle/permanently-delete! conn (:block/uuid page1))))
|
||||
(is (nil? (d/entity @conn [:block/uuid block-uuid])))))
|
||||
|
||||
(deftest permanently-delete-recycled-converted-page-removes-property-value-blocks
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "outer"}
|
||||
:blocks [{:block/title "target"
|
||||
:build/properties {:default "value"}}]}])
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
target-id (:db/id target)
|
||||
target-uuid (:block/uuid target)
|
||||
value-id (d/q '[:find ?value .
|
||||
:in $ ?target
|
||||
:where
|
||||
[?value :block/parent ?target]
|
||||
[?value :logseq.property/created-from-property]]
|
||||
@conn target-id)]
|
||||
(d/transact! conn [[:db/retract target-id :block/page]
|
||||
[:db/add target-id :block/name "target"]
|
||||
[:db/add target-id :block/tags :logseq.class/Page]])
|
||||
(let [page (d/entity @conn target-id)]
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page}))
|
||||
(is (true? (recycle/permanently-delete! conn target-uuid)))
|
||||
(is (nil? (d/entity @conn target-id)))
|
||||
(is (nil? (d/entity @conn value-id)))))
|
||||
|
||||
(deftest gc-recycled-converted-page-removes-property-value-blocks
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "outer"}
|
||||
:blocks [{:block/title "target"
|
||||
:build/properties {:default "value"}}
|
||||
{:block/title "unrelated"}]}])
|
||||
outer (ldb/get-page @conn "outer")
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
unrelated (db-test/find-block-by-content @conn "unrelated")
|
||||
target-id (:db/id target)
|
||||
value-id (d/q '[:find ?value .
|
||||
:in $ ?target
|
||||
:where
|
||||
[?value :block/parent ?target]
|
||||
[?value :logseq.property/created-from-property]]
|
||||
@conn target-id)]
|
||||
(d/transact! conn [[:db/retract target-id :block/page]
|
||||
[:db/add target-id :block/name "target"]
|
||||
[:db/add target-id :block/tags :logseq.class/Page]])
|
||||
(let [page (d/entity @conn target-id)]
|
||||
(ldb/transact! conn
|
||||
(recycle/recycle-page-tx-data @conn page {:now-ms 0})
|
||||
{:outliner-op :delete-page}))
|
||||
(is (true? (recycle/gc! conn {:now-ms (* 31 24 3600 1000)})))
|
||||
(is (nil? (d/entity @conn target-id)))
|
||||
(is (nil? (d/entity @conn value-id)))
|
||||
(is (some? (d/entity @conn (:db/id outer))))
|
||||
(is (some? (d/entity @conn (:db/id unrelated))))))
|
||||
|
||||
(deftest gc-keeps-unexpired-recycled-page
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
page-id (:db/id page)]
|
||||
(ldb/transact! conn
|
||||
(recycle/recycle-page-tx-data @conn page {:now-ms 0})
|
||||
{:outliner-op :delete-page})
|
||||
(is (nil? (recycle/gc! conn {:now-ms (* 29 24 3600 1000)})))
|
||||
(is (some? (d/entity @conn page-id)))))
|
||||
|
||||
(deftest apply-ops-permanently-delete-recycled-page-removes-page-and-descendants
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
block (db-test/find-block-by-content @conn "b1")
|
||||
page-uuid (:block/uuid page)
|
||||
block-uuid (:block/uuid block)]
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid page-uuid]))))
|
||||
(outliner-op/apply-ops! conn [[:recycle-delete-permanently [page-uuid]]] {})
|
||||
(is (nil? (d/entity @conn [:block/uuid page-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid block-uuid])))))
|
||||
|
||||
(deftest permanently-delete-recycled-block-removes-subtree-only
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
parent (db-test/find-block-by-content @conn "parent")
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
parent-uuid (:block/uuid parent)
|
||||
child-uuid (:block/uuid child)]
|
||||
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [parent] {}) {:outliner-op :delete-blocks})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid parent-uuid]))))
|
||||
(is (true? (recycle/permanently-delete! conn parent-uuid)))
|
||||
(is (some? (d/entity @conn [:block/uuid (:block/uuid page)])))
|
||||
(is (nil? (d/entity @conn [:block/uuid parent-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
|
||||
|
||||
(deftest apply-ops-permanently-delete-recycled-block-removes-subtree-only
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}])
|
||||
parent (db-test/find-block-by-content @conn "parent")
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
parent-uuid (:block/uuid parent)
|
||||
child-uuid (:block/uuid child)]
|
||||
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [parent] {}) {:outliner-op :delete-blocks})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid parent-uuid]))))
|
||||
(outliner-op/apply-ops! conn [[:recycle-delete-permanently [parent-uuid]]] {})
|
||||
(is (nil? (d/entity @conn [:block/uuid parent-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
|
||||
|
||||
(deftest permanently-delete-recycled-block-removes-corresponding-view-history
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "target"}]}])
|
||||
target (db-test/find-block-by-content @conn "target")
|
||||
target-uuid (:block/uuid target)
|
||||
view-uuid (random-uuid)
|
||||
target-history-uuid (random-uuid)
|
||||
view-history-uuid (random-uuid)
|
||||
now (common-util/time-ms)
|
||||
_ (d/transact! conn [{:block/uuid view-uuid
|
||||
:block/title "target view"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:logseq.property/view-for (:db/id target)
|
||||
:logseq.property.view/type :logseq.property.view/type.table
|
||||
:logseq.property.view/feature-type :linked-references}
|
||||
{:block/uuid target-history-uuid
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:logseq.property.history/block (:db/id target)
|
||||
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
|
||||
:logseq.property.history/scalar-value "Todo"}
|
||||
{:block/uuid view-history-uuid
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:logseq.property.history/block [:block/uuid view-uuid]
|
||||
:logseq.property.history/property (:db/id (d/entity @conn :logseq.property/status))
|
||||
:logseq.property.history/scalar-value "List"}])]
|
||||
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [target] {}) {:outliner-op :delete-blocks})
|
||||
(is (true? (recycle/permanently-delete! conn target-uuid)))
|
||||
(is (nil? (d/entity @conn [:block/uuid target-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid view-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid target-history-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid view-history-uuid])))))
|
||||
18
deps/outliner/test/logseq/outliner/tree_test.cljs
vendored
Normal file
18
deps/outliner/test/logseq/outliner/tree_test.cljs
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
(ns logseq.outliner.tree-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[logseq.outliner.tree :as otree]))
|
||||
|
||||
(deftest blocks->vec-tree-data-preserves-caller-field-policy
|
||||
(let [root {:db/id 1}
|
||||
child {:db/id 2
|
||||
:block/uuid (random-uuid)
|
||||
:block/order "a0"
|
||||
:block/parent {:db/id 1}
|
||||
:block/tx-id 9}
|
||||
default-result (otree/blocks->vec-tree-data [child] root {:include-root? false})
|
||||
renderer-result (otree/blocks->vec-tree-data [child] root {:include-root? false
|
||||
:keep-block-tx-id? true})]
|
||||
(is (vector? default-result))
|
||||
(is (= [1] (mapv :block/level default-result)))
|
||||
(is (nil? (:block/tx-id (first default-result))))
|
||||
(is (= 9 (:block/tx-id (first renderer-result))))))
|
||||
308
deps/outliner/test/logseq/outliner/validate_test.cljs
vendored
Normal file
308
deps/outliner/test/logseq/outliner/validate_test.cljs
vendored
Normal file
@@ -0,0 +1,308 @@
|
||||
(ns logseq.outliner.validate-test
|
||||
(:require [cljs.test :refer [are deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[logseq.db.common.entity-plus :as entity-plus]
|
||||
[logseq.db.frontend.entity-util :as entity-util]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.validate :as outliner-validate]))
|
||||
|
||||
(deftest validate-block-title-unique-for-properties
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:color {:logseq.property/type :default}
|
||||
:color2 {:logseq.property/type :default}}})]
|
||||
|
||||
(is (nil?
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
(:block/title (d/entity @conn :logseq.property/background-color))
|
||||
(d/entity @conn :user.property/color)))
|
||||
"Allow user property to have same name as built-in property")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate property"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"color"
|
||||
(d/entity @conn :user.property/color2)))
|
||||
"Disallow duplicate user property")))
|
||||
|
||||
(deftest validate-block-title-unique-for-tags
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:Class1 {}
|
||||
:Class2 {:logseq.property.class/extends :logseq.class/Task}}})]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate class"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Class1"
|
||||
(d/entity @conn :user.class/Class2)))
|
||||
"Disallow duplicate class names, regardless of extends")
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate class"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Card"
|
||||
(d/entity @conn :user.class/Class1)))
|
||||
"Disallow duplicate class names even if it's built-in")))
|
||||
|
||||
(deftest validate-block-title-unique-for-namespaced-pages
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "Library"
|
||||
:block/uuid #uuid "d246c71a-3e71-42f0-928f-afe607ee5ce0"
|
||||
:build/keep-uuid? true
|
||||
:build/properties {:logseq.property/built-in? true}}}
|
||||
{:page {:block/title "n1"
|
||||
:block/uuid #uuid "3aa1e950-5a9b-4efc-81d4-b6d89a504591"
|
||||
:build/keep-uuid? true
|
||||
:block/parent [:block/uuid #uuid "d246c71a-3e71-42f0-928f-afe607ee5ce0"]}}
|
||||
{:page {:block/title "n2"
|
||||
:block/parent [:block/uuid #uuid "3aa1e950-5a9b-4efc-81d4-b6d89a504591"]}}
|
||||
{:page {:block/title "n3"
|
||||
:block/parent [:block/uuid #uuid "3aa1e950-5a9b-4efc-81d4-b6d89a504591"]}}]
|
||||
:build-existing-tx? true})]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate page"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"n2"
|
||||
(db-test/find-page-by-title @conn "n3")))
|
||||
"Disallow duplicate namespace child")
|
||||
|
||||
(is (nil?
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"n4"
|
||||
(db-test/find-page-by-title @conn "n3")))
|
||||
"Allow namespace child if unique")))
|
||||
|
||||
(deftest validate-block-title-unique-for-pages
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}}
|
||||
{:page {:block/title "another page"}}
|
||||
{:page {:block/title "Apple" :build/tags [:Company]}}
|
||||
{:page {:block/title "Another Company" :build/tags [:Company]}}
|
||||
{:page {:block/title "Banana" :build/tags [:Fruit]}}])]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate page"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Apple"
|
||||
(db-test/find-page-by-title @conn "Another Company")))
|
||||
"Disallow duplicate page with tag")
|
||||
(is (nil?
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Apple"
|
||||
(db-test/find-page-by-title @conn "Banana")))
|
||||
"Allow page with same name for different tag")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Duplicate page"
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"page1"
|
||||
(db-test/find-page-by-title @conn "another page")))
|
||||
"Disallow duplicate page without tag")
|
||||
|
||||
(is (nil?
|
||||
(outliner-validate/validate-unique-by-name-and-tags
|
||||
@conn
|
||||
"Apple"
|
||||
(db-test/find-page-by-title @conn "Fruit")))
|
||||
"Allow class to have same name as a page")))
|
||||
|
||||
(deftest validate-extends-property
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:prop1 {:logseq.property/type :default}}
|
||||
:classes {:Class1 {} :Class2 {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}}
|
||||
{:page {:block/title "page2"}}]})
|
||||
page1 (db-test/find-page-by-title @conn "page1")
|
||||
class1 (db-test/find-page-by-title @conn "Class1")
|
||||
class2 (db-test/find-page-by-title @conn "Class2")
|
||||
property (db-test/find-page-by-title @conn "prop1")
|
||||
db @conn]
|
||||
|
||||
(testing "valid parent and child combinations"
|
||||
(is (nil? (outliner-validate/validate-extends-property db class1 [class2]))
|
||||
"parent class to child class is valid"))
|
||||
|
||||
(testing "invalid parent and child combinations"
|
||||
(are [parent child]
|
||||
(thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't extend"
|
||||
(outliner-validate/validate-extends-property db parent [child]))
|
||||
|
||||
class1 page1
|
||||
page1 class1
|
||||
property class1))
|
||||
|
||||
(testing "built-in tag can't have parent changed"
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't change.*built-in"
|
||||
(outliner-validate/validate-extends-property db
|
||||
(entity-plus/entity-memoized @conn :logseq.class/Task)
|
||||
[(entity-plus/entity-memoized @conn :logseq.class/Cards)]))))))
|
||||
|
||||
(deftest validate-tags-property
|
||||
(let [class-uuid (random-uuid)
|
||||
conn (db-test/create-conn-with-blocks
|
||||
{:classes {:SomeTag {:block/uuid class-uuid :build/keep-uuid? true}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "block"
|
||||
:build/children [{:block/title "block - invalid location"}]}
|
||||
{:block/title "block / invalid title"}]}
|
||||
{:page {:block/uuid class-uuid}
|
||||
:blocks [{:block/title "class block"}]}]
|
||||
:build-existing-tx? true})
|
||||
block (db-test/find-block-by-content @conn "block")
|
||||
block-invalid-title (db-test/find-block-by-content @conn #"invalid title")
|
||||
block-invalid-location (db-test/find-block-by-content @conn #"invalid location")]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't add tag.*Tag"
|
||||
(outliner-validate/validate-tags-property @conn [:logseq.class/Tag] :user.class/SomeTag))
|
||||
"built-in tag must not be tagged by the user")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't add tag.*Heading"
|
||||
(outliner-validate/validate-tags-property @conn [:logseq.property/heading] :user.class/SomeTag))
|
||||
"built-in property must not be tagged by the user")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't add tag.*Contents"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id (db-test/find-page-by-title @conn "Contents"))] :user.class/SomeTag))
|
||||
"built-in page must not be tagged by the user")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't set tag.*Tag"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.class/Tag))
|
||||
"Nodes can't be tagged with built-in private tags")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't set tag.*Priority"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.property/priority))
|
||||
"Nodes can't be tagged with built-in non tags")
|
||||
|
||||
(is (nil? (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.class/Page))
|
||||
"Blocks can be tagged with #Page")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Page name can't.*/"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id block-invalid-title)] :logseq.class/Page))
|
||||
"Block with invalid title can't be tagged with #Page")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't convert this block to page"
|
||||
(outliner-validate/validate-tags-property @conn [(:db/id block-invalid-location)] :logseq.class/Page))
|
||||
"Block with invalid location can't be tagged with #Page")))
|
||||
|
||||
(deftest validate-tags-property-deletion
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:classes {:SomeTag {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "block" :build/tags [:logseq.class/Page]}]}]})
|
||||
page (db-test/find-page-by-title @conn "page1")
|
||||
page-with-parent (db-test/find-block-by-content @conn "block")]
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't remove tag.*Task"
|
||||
(outliner-validate/validate-tags-property-deletion @conn [(:db/id (d/entity @conn :logseq.class/Task))] :logseq.class/Tag))
|
||||
"built-in class must not have tag deleted by the user")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"Can't remove tag.*Tag"
|
||||
(outliner-validate/validate-tags-property-deletion @conn [(:db/id (d/entity @conn :user.class/SomeTag))] :logseq.class/Tag))
|
||||
"Node can't have private tag deleted by user")
|
||||
|
||||
(is (nil? (outliner-validate/validate-tags-property-deletion @conn [(:db/id page-with-parent)] :logseq.class/Page))
|
||||
"Page with parent can remove #Page")
|
||||
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"This page cannot be converted"
|
||||
(outliner-validate/validate-tags-property-deletion @conn [(:db/id page)] :logseq.class/Page))
|
||||
"Page without parent can't remove #Page")))
|
||||
|
||||
(deftest validate-editing-built-in-property
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:myprop {:logseq.property/type :default}}})
|
||||
user-prop (d/entity @conn :user.property/myprop)
|
||||
built-in-prop (d/entity @conn :block/tags)]
|
||||
|
||||
(testing "user property can edit any attribute"
|
||||
(is (nil? (outliner-validate/validate-editing-built-in-property
|
||||
user-prop {:db/cardinality :db.cardinality/many}))))
|
||||
|
||||
(testing "built-in property cannot edit disallowed attribute"
|
||||
(is (thrown-with-msg?
|
||||
js/Error
|
||||
#"not editable"
|
||||
(outliner-validate/validate-editing-built-in-property
|
||||
built-in-prop {:block/title "renamed"}))))
|
||||
|
||||
(testing "built-in property can edit allowed attribute"
|
||||
(is (nil? (outliner-validate/validate-editing-built-in-property
|
||||
built-in-prop {:logseq.property/hide-empty-value true}))))))
|
||||
|
||||
;; Try as many of the validations against a new graph to confirm
|
||||
;; that validations make sense and are valid for a new graph
|
||||
(deftest new-graph-should-be-valid
|
||||
(let [conn (db-test/create-conn)]
|
||||
|
||||
(testing "Validate pages"
|
||||
(let [pages (->> (d/q '[:find [?b ...] :where
|
||||
[?b :block/title]
|
||||
[?b :block/tags]] @conn)
|
||||
(map (fn [id]
|
||||
(d/entity @conn id))))
|
||||
page-errors (atom {})]
|
||||
(doseq [page pages]
|
||||
(try
|
||||
(outliner-validate/validate-unique-by-name-and-tags @conn (:block/title page) page)
|
||||
(outliner-validate/validate-page-title (:block/title page) {:node page})
|
||||
(outliner-validate/validate-page-title-characters (:block/title page) {:node page})
|
||||
(when (entity-util/property? page) (outliner-validate/validate-property-title (:block/title page)))
|
||||
(when (entity-util/class? page)
|
||||
(doseq [parent (:logseq.property.class/extends page)]
|
||||
(outliner-validate/validate-extends-property @conn parent [page] {:built-in? false})))
|
||||
|
||||
(catch :default e
|
||||
(if (= :notification (:type (ex-data e)))
|
||||
(swap! page-errors update (select-keys page [:block/title :db/ident :block/uuid]) (fnil conj []) e)
|
||||
(throw e)))))
|
||||
(is (= {} @page-errors)
|
||||
"Default pages shouldn't have any validation errors")))
|
||||
|
||||
(testing "Validate property relationships"
|
||||
(let [parent-child-pairs (d/q '[:find ?parent ?child
|
||||
:where [?child :logseq.property.class/extends ?parent]] @conn)]
|
||||
(doseq [[parent-id child-id] parent-child-pairs]
|
||||
(let [parent (d/entity @conn parent-id)
|
||||
child (d/entity @conn child-id)]
|
||||
(is (nil? (#'outliner-validate/validate-extends-property-have-correct-type parent [child]))
|
||||
(str "Parent and child page is valid: " (pr-str (:block/title parent)) " " (pr-str (:block/title child))))))))))
|
||||
Reference in New Issue
Block a user