plugins_basic_test.clj 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. (ns logseq.e2e.plugins-basic-test
  2. (:require
  3. [clojure.set :as set]
  4. [clojure.string :as string]
  5. [clojure.test :refer [deftest testing is use-fixtures]]
  6. [jsonista.core :as json]
  7. [logseq.e2e.assert :as assert]
  8. [logseq.e2e.fixtures :as fixtures]
  9. [logseq.e2e.keyboard :as k]
  10. [logseq.e2e.page :as page]
  11. [logseq.e2e.util :as util]
  12. [wally.main :as w]
  13. [wally.repl :as repl]))
  14. (use-fixtures :once fixtures/open-page)
  15. (use-fixtures :each fixtures/new-logseq-page)
  16. (defn ->plugin-ident
  17. [property-name]
  18. (str ":plugin.property._test_plugin/" property-name))
  19. (defn- to-snake-case
  20. "Converts a string to snake_case. Handles camelCase, PascalCase, spaces, hyphens, and existing underscores.
  21. Examples:
  22. 'HelloWorld' -> 'hello_world'
  23. 'Hello World' -> 'hello_world'
  24. 'hello-world' -> 'hello_world'
  25. 'Hello__World' -> 'hello_world'"
  26. [s]
  27. (when (string? s)
  28. (-> s
  29. ;; Normalize input: replace hyphens/spaces with underscores, collapse multiple underscores
  30. (clojure.string/replace #"[-\s]+" "_")
  31. ;; Split on uppercase letters (except at start) and join with underscore
  32. (clojure.string/replace #"(?<!^)([A-Z])" "_$1")
  33. ;; Remove redundant underscores and trim
  34. (clojure.string/replace #"_+" "_")
  35. (clojure.string/trim)
  36. ;; Convert to lowercase
  37. (clojure.string/lower-case))))
  38. (defonce ^:private *property-idx (atom 0))
  39. (defn- new-property
  40. []
  41. (str "p" (swap! *property-idx inc)))
  42. (defn- ls-api-call!
  43. [tag & args]
  44. (let [tag (name tag)
  45. ns' (string/split tag #"\.")
  46. ns? (and (seq ns') (= (count ns') 2))
  47. inbuilt? (contains? #{"app" "editor"} (first ns'))
  48. ns1 (string/lower-case (if (and ns? (not inbuilt?))
  49. (str "sdk." (first ns')) "api"))
  50. name1 (if ns? (to-snake-case (last ns')) tag)
  51. estr (format "s => { const args = JSON.parse(s);const o=logseq.%1$s; return o['%2$s']?.apply(null, args || []); }" ns1 name1)
  52. args (json/write-value-as-string (vec args))]
  53. ;; (prn "Debug: eval-js #" estr args)
  54. (w/eval-js estr args)))
  55. (defn- assert-api-ls-block!
  56. ([ret] (assert-api-ls-block! ret 1))
  57. ([ret-or-uuid count]
  58. (let [uuid' (or (get ret-or-uuid "uuid") ret-or-uuid)]
  59. (is (string? uuid'))
  60. (assert/assert-have-count (str "#ls-block-" uuid') count)
  61. uuid')))
  62. (deftest editor-apis-test
  63. (testing "editor related apis"
  64. (page/new-page "test-block-apis")
  65. (ls-api-call! :ui.showMsg "hello world" "info")
  66. (let [ret (ls-api-call! :editor.appendBlockInPage "test-block-apis" "append-block-in-page-0")
  67. ret1 (ls-api-call! :editor.appendBlockInPage "append-block-in-current-page-0")
  68. uuid' (assert-api-ls-block! ret)]
  69. (assert-api-ls-block! ret1)
  70. (-> (ls-api-call! :editor.insertBlock uuid' "insert-0")
  71. (assert-api-ls-block!))
  72. (ls-api-call! :editor.updateBlock uuid' "append-but-updated-0")
  73. (k/esc)
  74. (w/wait-for ".block-title-wrap:text('append-but-updated-0')")
  75. (ls-api-call! :editor.removeBlock uuid')
  76. (assert-api-ls-block! uuid' 0))))
  77. (deftest block-properties-test
  78. (testing "block properties related apis"
  79. (page/new-page "test-block-properties-apis")
  80. (let [ret (ls-api-call! :editor.appendBlockInPage "test-block-properties-apis" "block-in-page-0" {:properties {:p1 1}})
  81. uuid' (assert-api-ls-block! ret)
  82. prop1 (ls-api-call! :editor.getBlockProperty uuid' "p1")
  83. props1 (ls-api-call! :editor.getBlockProperties uuid' "p1")
  84. props2 (ls-api-call! :editor.getPageProperties "test-block-properties-apis")]
  85. (w/wait-for ".property-k:text('p1')")
  86. (is (= 1 (get prop1 "value")))
  87. (is (= (get prop1 "ident") ":plugin.property._test_plugin/p1"))
  88. (is (= 1 (get props1 ":plugin.property._test_plugin/p1")))
  89. (is (= ["Page"] (get props2 ":block/tags")))
  90. (ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2")
  91. (ls-api-call! :editor.upsertBlockProperty uuid' "p3" true)
  92. (ls-api-call! :editor.upsertBlockProperty uuid' "p4" {:a 1, :b [2, 3]})
  93. (let [prop2 (ls-api-call! :editor.getBlockProperty uuid' "p2")
  94. prop3 (ls-api-call! :editor.getBlockProperty uuid' "p3")
  95. prop4 (ls-api-call! :editor.getBlockProperty uuid' "p4")]
  96. (w/wait-for ".property-k:text('p2')")
  97. (is (= "p2" (get prop2 "value")))
  98. (is (true? prop3))
  99. (is (= prop4 {"a" 1, "b" [2 3]})))
  100. (ls-api-call! :editor.removeBlockProperty uuid' "p4")
  101. ;; wait for react re-render
  102. (util/wait-timeout 16)
  103. (is (nil? (w/find-one-by-text ".property-k" "p4")))
  104. (ls-api-call! :editor.upsertBlockProperty uuid' "p3" false)
  105. (ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2-updated")
  106. (w/wait-for ".block-title-wrap:text('p2-updated')")
  107. (let [props (ls-api-call! :editor.getBlockProperties uuid')]
  108. (is (= (get props ":plugin.property._test_plugin/p3") false))
  109. (is (= (get props ":plugin.property._test_plugin/p2") "p2-updated"))))))
  110. (deftest property-upsert-test
  111. (testing "property with default settings"
  112. (let [p (new-property)]
  113. (ls-api-call! :editor.upsertProperty p)
  114. (let [property (ls-api-call! :editor.getProperty p)]
  115. (is (= "default" (get property "type")))
  116. (is (= ":db.cardinality/one" (get property "cardinality"))))))
  117. (testing "property with specified cardinality && type"
  118. (let [p (new-property)]
  119. (ls-api-call! :editor.upsertProperty p {:type "number"
  120. :cardinality "one"})
  121. (let [property (ls-api-call! :editor.getProperty p)]
  122. (is (= "number" (get property "type")))
  123. (is (= ":db.cardinality/one" (get property "cardinality")))))
  124. (let [p (new-property)]
  125. (ls-api-call! :editor.upsertProperty p {:type "number"
  126. :cardinality "many"})
  127. (let [property (ls-api-call! :editor.getProperty p)]
  128. (is (= "number" (get property "type")))
  129. (is (= ":db.cardinality/many" (get property "cardinality"))))
  130. (ls-api-call! :editor.upsertProperty p {:type "default"})
  131. (let [property (ls-api-call! :editor.getProperty p)]
  132. (is (= "default" (get property "type"))))))
  133. ;; TODO: How to test against eval-js errors on playwright?
  134. #_(testing ":checkbox property doesn't allow :many cardinality"
  135. (let [p (new-property)]
  136. (ls-api-call! :editor.upsertProperty p {:type "checkbox"
  137. :cardinality "many"}))))
  138. (deftest property-related-test
  139. (testing "properties management related apis"
  140. (dorun
  141. (map-indexed
  142. (fn [idx property-type]
  143. (let [property-name (str "p" idx)
  144. _ (ls-api-call! :editor.upsertProperty property-name {:type property-type})
  145. property (ls-api-call! :editor.getProperty property-name)]
  146. (is (= (get property "ident") (str ":plugin.property._test_plugin/" property-name)))
  147. (is (= (get property "type") property-type))
  148. (ls-api-call! :editor.removeProperty property-name)
  149. (is (nil? (ls-api-call! :editor.getProperty property-name)))))
  150. ["default" "number" "date" "datetime" "checkbox" "url" "node" "json" "string"]))))
  151. (deftest insert-block-with-properties
  152. (testing "insert block with properties"
  153. (let [page "insert-block-properties-test"
  154. _ (page/new-page page)
  155. ;; :checkbox, :number, :url, :json can be inferred and default to :default, but not for :page
  156. b1 (ls-api-call! :editor.insertBlock page "b1" {:properties {"x1" true
  157. "x2" "https://logseq.com"
  158. "x3" 1
  159. "x4" [1]
  160. "x5" {:foo "bar"}
  161. "x6" "Page x"
  162. "x7" ["Page y" "Page z"]
  163. "x8" "some content"}
  164. :schema {"x6" {:type "page"}
  165. "x7" {:type "page"}}})]
  166. (is (true? (get b1 (->plugin-ident "x1"))))
  167. (is (= "https://logseq.com" (-> (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x2")))
  168. (get "title"))))
  169. (is (= 1 (-> (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x3")))
  170. (get ":logseq.property/value"))))
  171. (is (= 1 (-> (ls-api-call! :editor.getBlock (first (get b1 (->plugin-ident "x4"))))
  172. (get ":logseq.property/value"))))
  173. (is (= "{\"foo\":\"bar\"}" (get b1 (->plugin-ident "x5"))))
  174. (let [page-x (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x6")))]
  175. (is (= "page x" (get page-x "name"))))
  176. (is (= ["page y" "page z"] (map #(-> (ls-api-call! :editor.getBlock %)
  177. (get "name")) (get b1 (->plugin-ident "x7")))))
  178. (let [x8-block-value (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x8")))]
  179. (is (= "some content" (get x8-block-value "title")))
  180. (is (some? (get x8-block-value "page")))))))
  181. (deftest update-block-with-properties
  182. (testing "update block with properties"
  183. (let [page "update-block-properties-test"
  184. _ (page/new-page page)
  185. block (ls-api-call! :editor.insertBlock page "b1")
  186. _ (ls-api-call! :editor.updateBlock (get block "uuid")
  187. "b1-new-content"
  188. {:properties {"y1" true
  189. "y2" "https://logseq.com"
  190. "y3" 1
  191. "y4" [1]
  192. "y5" {:foo "bar"}
  193. "y6" "Page x"
  194. "y7" ["Page y" "Page z"]
  195. "y8" "some content"}
  196. :schema {"y6" {:type "page"}
  197. "y7" {:type "page"}}})
  198. b1 (ls-api-call! :editor.getBlock (get block "uuid"))]
  199. (is (true? (get b1 (->plugin-ident "y1"))))
  200. (is (= "https://logseq.com" (-> (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y2") "id"]))
  201. (get "title"))))
  202. (is (= 1 (-> (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y3") "id"]))
  203. (get ":logseq.property/value"))))
  204. (is (= 1 (-> (ls-api-call! :editor.getBlock (get (first (get b1 (->plugin-ident "y4"))) "id"))
  205. (get ":logseq.property/value"))))
  206. (is (= "{\"foo\":\"bar\"}" (get b1 (->plugin-ident "y5"))))
  207. (let [page-x (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y6") "id"]))]
  208. (is (= "page x" (get page-x "name"))))
  209. (is (= ["page y" "page z"] (map #(-> (ls-api-call! :editor.getBlock %)
  210. (get "name"))
  211. (map #(get % "id") (get b1 (->plugin-ident "y7"))))))
  212. (let [y8-block-value (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y8") "id"]))]
  213. (is (= "some content" (get y8-block-value "title")))
  214. (is (some? (get y8-block-value "page")))))))
  215. (deftest insert-batch-blocks-test
  216. (testing "insert batch blocks"
  217. (let [page "insert batch blocks"
  218. _ (page/new-page page)
  219. page-uuid (get (ls-api-call! :editor.getBlock page) "uuid")
  220. result (ls-api-call! :editor.insertBatchBlock page-uuid
  221. [{:content "b1"
  222. :children [{:content "b1.1"
  223. :children [{:content "b1.1.1"}
  224. {:content "b1.1.2"}]}
  225. {:content "b1.2"}]}
  226. {:content "b2"}])
  227. contents (util/get-page-blocks-contents)]
  228. (is (= contents ["b1" "b1.1" "b1.1.1" "b1.1.2" "b1.2" "b2"]))
  229. (is (= (map #(get % "title") result) ["b1" "b1.1" "b1.1.1" "b1.1.2" "b1.2" "b2"]))))
  230. (testing "insert batch blocks with properties"
  231. (let [page "insert batch blocks with properties"
  232. _ (page/new-page page)
  233. page-uuid (get (ls-api-call! :editor.getBlock page) "uuid")
  234. result (ls-api-call! :editor.insertBatchBlock page-uuid
  235. [{:content "b1"
  236. :children [{:content "b1.1"
  237. :children [{:content "b1.1.1"
  238. :properties {"z3" "Page 1"
  239. "z4" ["Page 2" "Page 3"]}}
  240. {:content "b1.1.2"}]}
  241. {:content "b1.2"}]
  242. :properties {"z1" "test"
  243. "z2" true}}
  244. {:content "b2"}]
  245. {:schema {"z3" "page"
  246. "z4" "page"}})
  247. contents (util/get-page-blocks-contents)]
  248. (is (= contents
  249. ["b1" "test" "b1.1" "b1.1.1" "Page 1" "Page 2" "Page 3" "b1.1.2" "b1.2" "b2"]))
  250. (is (true? (get (first result) (->plugin-ident "z2")))))))
  251. (deftest create-page-test
  252. (testing "create page"
  253. (let [result (ls-api-call! :editor.createPage "Test page 1")]
  254. (is (= "Test page 1" (get result "title")))
  255. (is
  256. (=
  257. ":logseq.class/Page"
  258. (-> (ls-api-call! :editor.getBlock (first (get result "tags")))
  259. (get "ident"))))))
  260. (testing "create page with properties"
  261. (let [result (ls-api-call! :editor.createPage "Test page 2"
  262. {:px1 "test"
  263. :px2 1
  264. :px3 "Page 1"
  265. :px4 ["Page 2" "Page 3"]}
  266. {:schema {:px3 {:type "page"}
  267. :px4 {:type "page"}}})
  268. page (ls-api-call! :editor.getBlock "Test page 2")]
  269. (is (= "Test page 2" (get result "title")))
  270. (is
  271. (=
  272. ":logseq.class/Page"
  273. (-> (ls-api-call! :editor.getBlock (first (get result "tags")))
  274. (get "ident"))))
  275. ;; verify properties
  276. (is (= "test" (-> (ls-api-call! :editor.getBlock (get-in page [(->plugin-ident "px1") "id"]))
  277. (get "title"))))
  278. (is (= 1 (-> (ls-api-call! :editor.getBlock (get-in page [(->plugin-ident "px2") "id"]))
  279. (get ":logseq.property/value"))))
  280. (let [page-1 (ls-api-call! :editor.getBlock (get-in page [(->plugin-ident "px3") "id"]))]
  281. (is (= "page 1" (get page-1 "name"))))
  282. (is (= ["page 2" "page 3"] (map #(-> (ls-api-call! :editor.getBlock %)
  283. (get "name"))
  284. (map #(get % "id") (get page (->plugin-ident "px4"))))))))
  285. (testing "create tag page"
  286. (let [result (ls-api-call! :editor.createPage "Tag new"
  287. {}
  288. {:class true})]
  289. (is
  290. (=
  291. ":logseq.class/Tag"
  292. (-> (ls-api-call! :editor.getBlock (first (get result "tags")))
  293. (get "ident")))))))
  294. (deftest get-all-tags-test
  295. (testing "get_all_tags"
  296. (let [result (ls-api-call! :editor.get_all_tags)
  297. built-in-tags #{":logseq.class/Template"
  298. ":logseq.class/Query"
  299. ":logseq.class/Math-block"
  300. ":logseq.class/Task"
  301. ":logseq.class/Code-block"
  302. ":logseq.class/Card"
  303. ":logseq.class/Quote-block"
  304. ":logseq.class/Cards"}]
  305. (is (set/subset? built-in-tags (set (map #(get % "ident") result)))))))
  306. (deftest get-all-properties-test
  307. (testing "get_all_properties"
  308. (let [result (ls-api-call! :editor.get_all_properties)]
  309. (is (>= (count result) 94)))))
  310. (deftest get-tag-objects-test
  311. (testing "get_tag_objects"
  312. (let [page "tag objects test"
  313. _ (page/new-page page)
  314. _ (ls-api-call! :editor.insertBlock page "task 1"
  315. {:properties {"logseq.property/status" "Doing"}})
  316. result (ls-api-call! :editor.get_tag_objects "logseq.class/Task")]
  317. (is (= (count result) 1))
  318. (is (= "task 1" (get (first result) "title"))))))
  319. (deftest create-and-get-tag-test
  320. (testing "create and get tag with title or ident"
  321. (let [title "book1"
  322. title-ident (str :plugin.class._test_plugin/book1)
  323. tag1 (ls-api-call! :editor.createTag title)
  324. tag2 (ls-api-call! :editor.getTag title)
  325. tag3 (ls-api-call! :editor.getTag title-ident)
  326. tag4 (ls-api-call! :editor.getTag (get tag1 "uuid"))]
  327. (is (= (get tag1 "ident") title-ident) "create tag with title from test as plugin")
  328. (is (= (get tag2 "ident") title-ident) "get tag with title")
  329. (is (= (get tag3 "title") title) "get tag with ident")
  330. (is (= (get tag4 "title") title) "get tag with uuid")))
  331. (testing "add and remove tag extends"
  332. (let [tag1 (ls-api-call! :editor.createTag "tag1")
  333. tag2 (ls-api-call! :editor.createTag "tag2")
  334. tag3 (ls-api-call! :editor.createTag "tag3")
  335. id1 (get tag1 "id")
  336. id2 (get tag2 "id")
  337. id3 (get tag3 "id")
  338. _ (ls-api-call! :editor.addTagExtends id1 id2)
  339. tag1 (ls-api-call! :editor.getTag id1)]
  340. (is (= (get tag1 ":logseq.property.class/extends") [id2]) "tag1 extends tag2 with db id")
  341. (let [_ (ls-api-call! :editor.addTagExtends id1 id3)
  342. tag1 (ls-api-call! :editor.getTag id1)]
  343. (is (= (get tag1 ":logseq.property.class/extends") [id2 id3]) "tag1 extends tag2,tag3 with db ids")))))
  344. (deftest get-tags-by-name-test
  345. (testing "get tags by exact name"
  346. (let [tag-name "product"
  347. tag1 (ls-api-call! :editor.createTag tag-name)
  348. result (ls-api-call! :editor.getTagsByName tag-name)]
  349. (is (= 1 (count result)) "should return exactly one tag")
  350. (is (= (get tag1 "uuid") (get (first result) "uuid")) "should return the created tag")
  351. (is (= (get tag1 "title") (get (first result) "title")) "tag title should match")))
  352. (testing "get tags by name is case-insensitive"
  353. (let [tag-name "TestTag123"
  354. _ (ls-api-call! :editor.createTag tag-name)
  355. result-lower (ls-api-call! :editor.getTagsByName "testtag123")
  356. result-upper (ls-api-call! :editor.getTagsByName "TESTTAG123")
  357. result-mixed (ls-api-call! :editor.getTagsByName "TeStTaG123")]
  358. (is (= 1 (count result-lower)) "should find tag with lowercase search")
  359. (is (= 1 (count result-upper)) "should find tag with uppercase search")
  360. (is (= 1 (count result-mixed)) "should find tag with mixed case search")
  361. (is (= (get (first result-lower) "uuid") (get (first result-upper) "uuid")) "all searches should return same tag")
  362. (is (= (get (first result-lower) "uuid") (get (first result-mixed) "uuid")) "all searches should return same tag")))
  363. (testing "get tags by name returns empty array for non-existent tag"
  364. (let [result (ls-api-call! :editor.getTagsByName "NonExistentTag12345")]
  365. (is (empty? result) "should return empty array for non-existent tag")))
  366. (testing "get tags by name filters out non-tag pages"
  367. (let [page-name "regular-page"
  368. _ (page/new-page page-name)
  369. result (ls-api-call! :editor.getTagsByName page-name)]
  370. (is (empty? result) "should not return regular pages, only tags")))
  371. (testing "get tags by name with multiple tags having similar names"
  372. (let [tag1 (ls-api-call! :editor.createTag "category")
  373. tag2 (ls-api-call! :editor.createTag "Category")
  374. result (ls-api-call! :editor.getTagsByName "category")]
  375. ;; Due to case-insensitive name normalization, both tags should be the same
  376. (is (>= (count result) 1) "should return at least one tag")
  377. ;; Verify the result contains valid tag structure
  378. (is (string? (get (first result) "uuid")) "returned tag should have uuid")
  379. (is (string? (get (first result) "title")) "returned tag should have title"))))