plugins_basic_test.clj 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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/Pdf-annotation"
  301. ":logseq.class/Task"
  302. ":logseq.class/Code-block"
  303. ":logseq.class/Card"
  304. ":logseq.class/Quote-block"
  305. ":logseq.class/Cards"}]
  306. (is (set/subset? built-in-tags (set (map #(get % "ident") result)))))))
  307. (deftest get-all-properties-test
  308. (testing "get_all_properties"
  309. (let [result (ls-api-call! :editor.get_all_properties)]
  310. (is (>= (count result) 94)))))
  311. (deftest get-tag-objects-test
  312. (testing "get_tag_objects"
  313. (let [page "tag objects test"
  314. _ (page/new-page page)
  315. _ (ls-api-call! :editor.insertBlock page "task 1"
  316. {:properties {"logseq.property/status" "Doing"}})
  317. result (ls-api-call! :editor.get_tag_objects "logseq.class/Task")]
  318. (is (= (count result) 1))
  319. (is (= "task 1" (get (first result) "title"))))))