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:
78
clj-e2e/test/logseq/e2e/assets_basic_test.clj
Normal file
78
clj-e2e/test/logseq/e2e/assets_basic_test.clj
Normal file
@@ -0,0 +1,78 @@
|
||||
(ns logseq.e2e.assets-basic-test
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w])
|
||||
(:import
|
||||
(java.nio.file Paths)
|
||||
(java.util.function Consumer)))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each fixtures/new-logseq-page fixtures/validate-graph)
|
||||
|
||||
(defn- asset-path
|
||||
[relative-path]
|
||||
(-> (Paths/get (str "../" relative-path) (into-array String []))
|
||||
(.toAbsolutePath)
|
||||
(.normalize)))
|
||||
|
||||
(deftest image-upload-lightbox-and-resize-test
|
||||
(testing "uploaded images create stable asset blocks and retain resized metadata"
|
||||
(let [page (w/get-page)
|
||||
files [(asset-path "assets/icon.png")
|
||||
(asset-path "assets/splash.png")
|
||||
(asset-path "resources/img/logo.png")]
|
||||
pending-files (atom files)]
|
||||
(.onFileChooser
|
||||
page
|
||||
(reify Consumer
|
||||
(accept [_ chooser]
|
||||
(let [file (first @pending-files)]
|
||||
(swap! pending-files rest)
|
||||
(.setFiles chooser
|
||||
(into-array java.nio.file.Path [file]))))))
|
||||
(b/new-block "image uploads")
|
||||
(doseq [expected-count (range 1 (inc (count files)))]
|
||||
(util/input-command "Upload an asset")
|
||||
(assert/assert-have-count
|
||||
".ls-page-blocks .asset-container img"
|
||||
expected-count))
|
||||
(assert/assert-have-count ".ls-page-blocks .asset-container img" 3)
|
||||
(doseq [image (.all (w/-query ".ls-page-blocks .asset-container img"))]
|
||||
(is (not (string/blank? (.getAttribute image "src"))))
|
||||
(is (pos? (.evaluate image "image => image.naturalWidth"))))
|
||||
(let [first-image (.first (w/-query ".ls-page-blocks .asset-container img"))
|
||||
first-block (.first (w/-query ".ls-page-blocks .ls-block:has(.asset-container img)"))
|
||||
block-uuid (.getAttribute first-block "blockid")]
|
||||
(w/click first-image)
|
||||
(assert/assert-is-visible ".pswp.pswp--open")
|
||||
(.waitForFunction page "() => window.pswp?.opener?.isOpen" nil)
|
||||
(w/click (w/get-by-label "Close"))
|
||||
(assert/assert-is-hidden ".pswp.pswp--open")
|
||||
(.hover (.first (w/-query ".ls-page-blocks .ls-resize-image")))
|
||||
(let [handle (.first (w/-query ".ls-page-blocks .image-resize.handle-right"))
|
||||
target (.first (w/-query ".ls-page-blocks .block-content"))]
|
||||
(assert/assert-is-visible handle)
|
||||
(.dragTo handle target))
|
||||
(util/wait-timeout 250)
|
||||
(let [block (ls-api-call! :editor.getBlock block-uuid)
|
||||
metadata (get block "properties")]
|
||||
(is (some? metadata)))
|
||||
(w/refresh)
|
||||
(assert/assert-have-count ".ls-page-blocks .asset-container img" 3))
|
||||
(let [asset-tag (ls-api-call! :editor.getTag "logseq.class/Asset")]
|
||||
(ls-api-call! :app.pushState
|
||||
"page"
|
||||
{"name" (get asset-tag "uuid")}
|
||||
nil))
|
||||
(w/wait-for ".ls-view-body .ls-table-row")
|
||||
(assert/assert-have-count ".ls-view-body .ls-table-row" 3)
|
||||
(assert/assert-is-visible
|
||||
".ls-table-header-cell:has-text('File')")
|
||||
(assert/assert-is-hidden
|
||||
".ls-table-header-cell:has-text('checksum')"))))
|
||||
51
clj-e2e/test/logseq/e2e/bidirectional_properties_test.clj
Normal file
51
clj-e2e/test/logseq/e2e/bidirectional_properties_test.clj
Normal file
@@ -0,0 +1,51 @@
|
||||
(ns logseq.e2e.bidirectional-properties-test
|
||||
(:require [clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.page :as page]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(deftest bidirectional-properties-test
|
||||
(testing "shows reverse property references when a class enables bidirectional properties"
|
||||
(let [friend-prop "friend"
|
||||
person-tag "Person"
|
||||
project-tag "Project"
|
||||
target "Bob"
|
||||
container-page "Bidirectional Props"]
|
||||
(ls-api-call! :editor.createTag person-tag
|
||||
{:tagProperties [{:name friend-prop
|
||||
:schema {:type "node"}}]})
|
||||
(ls-api-call! :editor.createTag project-tag)
|
||||
(let [person (ls-api-call! :editor.getTag person-tag)
|
||||
person-uuid (get person "uuid")
|
||||
friend (ls-api-call! :editor.getPage friend-prop)]
|
||||
(ls-api-call! :editor.upsertBlockProperty (get friend "id")
|
||||
"logseq.property/classes"
|
||||
(get person "id"))
|
||||
(is (string? person-uuid))
|
||||
(ls-api-call! :editor.upsertBlockProperty person-uuid
|
||||
"logseq.property.class/bidirectional-property-title"
|
||||
"People")
|
||||
(ls-api-call! :editor.upsertBlockProperty person-uuid
|
||||
"logseq.property.class/enable-bidirectional?"
|
||||
true))
|
||||
(ls-api-call! :editor.createPage target)
|
||||
(ls-api-call! :editor.createPage container-page)
|
||||
(let [bob (ls-api-call! :editor.getPage target)
|
||||
bob-id (get bob "id")]
|
||||
(ls-api-call! :editor.insertBlock container-page (str "Alice #" person-tag)
|
||||
{:properties {friend-prop bob-id}})
|
||||
(ls-api-call! :editor.insertBlock container-page (str "Charlie #" project-tag)
|
||||
{:properties {friend-prop bob-id}}))
|
||||
|
||||
(page/goto-page target)
|
||||
(w/wait-for ".property-k:text('People')")
|
||||
(assert/assert-is-visible ".property-value .block-title-wrap:text('Alice')")
|
||||
(assert/assert-have-count ".property-k:text('Projects')" 0))))
|
||||
507
clj-e2e/test/logseq/e2e/block_property_basic_test.clj
Normal file
507
clj-e2e/test/logseq/e2e/block_property_basic_test.clj
Normal file
@@ -0,0 +1,507 @@
|
||||
(ns logseq.e2e.block-property-basic-test
|
||||
(:require
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each fixtures/new-logseq-page fixtures/validate-graph)
|
||||
|
||||
(defn- current-page-name
|
||||
[]
|
||||
(page/get-page-name))
|
||||
|
||||
(defn- add-property-through-ui!
|
||||
[property-name property-type]
|
||||
(util/input-command "Add property")
|
||||
(w/click "input[placeholder]")
|
||||
(util/input property-name)
|
||||
(w/click (w/get-by-text "New option:"))
|
||||
(w/click (loc/and "span" (util/get-by-text property-type true))))
|
||||
|
||||
(defn- block-value
|
||||
[uuid property-name]
|
||||
(let [result (ls-api-call! :editor.getBlockProperty uuid property-name)]
|
||||
(if (instance? java.util.Map result)
|
||||
(get result "value")
|
||||
result)))
|
||||
|
||||
(defn- open-flashcards!
|
||||
[]
|
||||
(util/double-esc)
|
||||
(when-not (w/visible? "#left-sidebar.is-open")
|
||||
(w/click "#left-menu")
|
||||
(w/wait-for "#left-sidebar.is-open"))
|
||||
(w/wait-for ".flashcards-nav")
|
||||
(w/click ".flashcards-nav a")
|
||||
(assert/assert-is-visible "#cards-modal"))
|
||||
|
||||
(defn- wait-for-property-type
|
||||
[property-name expected-type]
|
||||
(loop [remaining-attempts 20]
|
||||
(let [actual-type (get (ls-api-call! :editor.getProperty property-name)
|
||||
"type")]
|
||||
(if (or (= expected-type actual-type)
|
||||
(zero? remaining-attempts))
|
||||
actual-type
|
||||
(do
|
||||
(util/wait-timeout 50)
|
||||
(recur (dec remaining-attempts)))))))
|
||||
|
||||
(deftest references-embeds-and-mounted-instance-refresh-test
|
||||
(testing "page refs, embeds, linked refs and mounted copies share canonical updates"
|
||||
(let [target-page "block sample target"
|
||||
source-page "block sample source"]
|
||||
(page/new-page target-page)
|
||||
(let [target (ls-api-call! :editor.appendBlockInPage
|
||||
target-page
|
||||
"target parent")
|
||||
target-uuid (get target "uuid")]
|
||||
(ls-api-call! :editor.insertBlock
|
||||
target-uuid
|
||||
"target child"
|
||||
{:sibling false})
|
||||
(page/new-page source-page)
|
||||
(b/new-block (str "[[" target-page "]]"))
|
||||
(b/new-block "")
|
||||
(util/input-command "Node embed")
|
||||
(util/press-seq target-page)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ui__popover-content a.menu-link.chosen"
|
||||
:has-text target-page))
|
||||
(k/enter)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".embed-block" :has-text "target child"))
|
||||
(ls-api-call! :editor.openInRightSidebar target-uuid)
|
||||
(w/click
|
||||
(format ".cp__right-sidebar #block-content-%s" target-uuid))
|
||||
(w/fill util/editor-q "target parent updated")
|
||||
(util/exit-edit)
|
||||
(doseq [container ["#main-content-container" ".cp__right-sidebar"]]
|
||||
(assert/assert-is-visible
|
||||
(loc/filter container :has-text "target parent updated")))
|
||||
(page/goto-page target-page)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".references" :has-text source-page))))))
|
||||
|
||||
(deftest unlinked-reference-filter-and-breadcrumb-test
|
||||
(testing "unlinked-reference search updates results and breadcrumbs track renames"
|
||||
(let [target "reference sample target"
|
||||
include-page "reference include"
|
||||
exclude-page "reference exclude"]
|
||||
(page/new-page target)
|
||||
(let [parent (ls-api-call! :editor.appendBlockInPage
|
||||
target
|
||||
"target nested parent")
|
||||
parent-uuid (get parent "uuid")
|
||||
child (ls-api-call! :editor.insertBlock
|
||||
parent-uuid
|
||||
"target nested child"
|
||||
{:sibling false})
|
||||
child-uuid (get child "uuid")]
|
||||
(page/new-page include-page)
|
||||
(b/new-block (str target " plain mention"))
|
||||
(page/new-page exclude-page)
|
||||
(b/new-block (str target " another mention"))
|
||||
(page/goto-page target)
|
||||
(w/click ".unlinked-references .ls-foldable-title-control")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".unlinked-references" :has-text include-page))
|
||||
(w/click
|
||||
".unlinked-references button:has(.ls-icon-search)")
|
||||
(w/fill ".unlinked-references .view-action-search input" include-page)
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".unlinked-references .ls-view-body" :has-text exclude-page)
|
||||
0)
|
||||
(w/click (format "#dot-%s" child-uuid))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".breadcrumb" :has-text "target nested parent"))
|
||||
(ls-api-call! :editor.updateBlock parent-uuid "target renamed parent")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".breadcrumb" :has-text "target renamed parent"))))))
|
||||
|
||||
(deftest flashcard-rating-advances-once-test
|
||||
(testing "answer reveal and one rating advance to the next card and update due count"
|
||||
(let [page-name (current-page-name)]
|
||||
(ls-api-call! :editor.appendBlockInPage page-name "sample card one #Card")
|
||||
(ls-api-call! :editor.appendBlockInPage page-name "sample card two #Card")
|
||||
(open-flashcards!)
|
||||
(w/click "#card-answers")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "#cards-modal" :has-text "Again"))
|
||||
(let [before (util/get-text "#cards-modal")]
|
||||
(w/click "#card-good")
|
||||
(let [after (util/get-text "#cards-modal")]
|
||||
(is (not= before after))))
|
||||
(assert/assert-have-count "#cards-modal .card-rating-loading" 0))))
|
||||
|
||||
(deftest multi-target-comment-draft-edit-delete-test
|
||||
(testing "multi-target comments preserve drafts and support edit/delete lifecycle"
|
||||
(b/new-blocks ["comment sample a" "comment sample b"])
|
||||
(b/select-blocks 2)
|
||||
(util/search-and-click "Add comment")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-comments-area" :has-text "those blocks"))
|
||||
(w/fill ".ls-comment-add textarea" "draft sample comment")
|
||||
(k/esc)
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".ls-comment-row" :has-text "draft sample comment")
|
||||
0)
|
||||
(let [reply-placeholder
|
||||
(loc/filter ".ls-comment-reply-placeholder"
|
||||
:has-text "draft sample comment")]
|
||||
(assert/assert-is-visible reply-placeholder)
|
||||
(w/click reply-placeholder))
|
||||
(is (= "draft sample comment"
|
||||
(.inputValue (w/-query ".ls-comment-add textarea"))))
|
||||
(w/fill ".ls-comment-add textarea" "saved sample comment")
|
||||
(w/click ".ls-comment-submit")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-comment-row" :has-text "saved sample comment"))
|
||||
(w/click ".ls-comment-row button[aria-label='Click to edit']")
|
||||
(w/fill ".ls-comment-row textarea" "edited sample comment")
|
||||
(k/enter)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-comment-row" :has-text "edited sample comment"))
|
||||
(w/click ".ls-comment-row button[title='Delete']")
|
||||
(assert/assert-have-count ".ls-comment-row" 0)))
|
||||
|
||||
(deftest block-and-comment-reaction-toggle-test
|
||||
(testing "block and comment reactions use their own targets and toggle without duplicates"
|
||||
(b/new-blocks ["reaction sample a" "reaction sample b"])
|
||||
(b/select-blocks 2)
|
||||
(util/search-and-click "Add reaction")
|
||||
(w/fill ".ls-icon-picker input" "thumbs up")
|
||||
(w/click ".ls-icon-picker button:has(em-emoji[id='+1'])")
|
||||
(assert/assert-have-count
|
||||
".ls-page-blocks .ls-block-reactions button:has(em-emoji[id='+1'])"
|
||||
2)
|
||||
(w/click
|
||||
".ls-page-blocks .ls-block:has-text('reaction sample a') .ls-block-reactions button:has(em-emoji[id='+1'])")
|
||||
(assert/assert-have-count
|
||||
".ls-page-blocks .ls-block:has-text('reaction sample a') .ls-block-reactions button:has(em-emoji[id='+1'])"
|
||||
0)
|
||||
(w/click (loc/filter ".block-title-wrap" :has-text "reaction sample b"))
|
||||
(util/search-and-click "Add comment")
|
||||
(w/fill ".ls-comment-add textarea" "reaction comment")
|
||||
(w/click ".ls-comment-submit")
|
||||
(w/click ".ls-comment-row button[title='Add reaction']")
|
||||
(w/fill ".ls-icon-picker input" "red heart")
|
||||
(w/click ".ls-icon-picker button:has(em-emoji[id='heart'])")
|
||||
(assert/assert-have-count
|
||||
".ls-comment-row .ls-block-reactions button:has(em-emoji[id='heart'])"
|
||||
1)
|
||||
(assert/assert-have-count
|
||||
".ls-page-blocks .ls-block-reactions button:has(em-emoji[id='heart'])"
|
||||
1)))
|
||||
|
||||
(deftest icon-and-structural-tag-visibility-test
|
||||
(testing "icons update mounted references and internal tags stay hidden from user tags"
|
||||
(let [page-name "icon sample page"]
|
||||
(page/new-page page-name)
|
||||
(util/exit-edit)
|
||||
(w/click "button:text('Add icon')")
|
||||
(w/fill ".cp__emoji-icon-picker input" "books")
|
||||
(w/click ".cp__emoji-icon-picker button:has(em-emoji[id='books'])")
|
||||
(assert/assert-is-visible ".ls-page-icon")
|
||||
(b/new-block "icon sample block")
|
||||
(util/set-tag "task")
|
||||
(util/exit-edit)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".block-tag" :has-text "task"))
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".block-tag" :has-text "Page")
|
||||
0)
|
||||
(ls-api-call! :editor.openInRightSidebar
|
||||
(get (ls-api-call! :editor.getPage page-name) "uuid"))
|
||||
(assert/assert-is-visible
|
||||
".cp__right-sidebar .page-title em-emoji[id='books']")
|
||||
(w/click ".ls-page-icon button")
|
||||
(w/click ".cp__emoji-icon-picker button[data-action='del']")
|
||||
(assert/assert-have-count "em-emoji[id='books']" 0))))
|
||||
|
||||
(deftest property-create-and-name-validation-test
|
||||
(testing "property creation is unique and invalid names cannot create ghost nodes"
|
||||
(b/new-block "property validation owner")
|
||||
(add-property-through-ui! "valid-property" "Text")
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".property-k" :has-text "valid-property")
|
||||
1)
|
||||
(b/open-last-block)
|
||||
(util/input-command "Add property")
|
||||
(w/click "input[placeholder]")
|
||||
(assert/assert-have-count (util/get-by-text "New option:" false) 0)
|
||||
(k/esc)
|
||||
(doseq [invalid-name ["[[bad" "#bad"]]
|
||||
(b/open-last-block)
|
||||
(util/input-command "Add property")
|
||||
(w/click "input[placeholder]")
|
||||
(util/input invalid-name)
|
||||
(w/click (w/get-by-text "New option:"))
|
||||
(w/click (loc/and "span" (util/get-by-text "Text" true)))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ui__toast.error" :has-text "invalid property name"))
|
||||
(doseq [message ["Property failed to create"
|
||||
"invalid property name"]]
|
||||
(let [toast (loc/filter ".ui__toast.error" :has-text message)]
|
||||
(when (w/visible? toast)
|
||||
(w/click (.locator toast ".ui__toast-close")))))
|
||||
(k/esc))
|
||||
(is (nil? (ls-api-call! :editor.getProperty "[[bad")))
|
||||
(is (nil? (ls-api-call! :editor.getProperty "#bad")))))
|
||||
|
||||
(deftest scalar-property-value-validation-test
|
||||
(testing "number/date reject incompatible text and URL values preserve valid edits"
|
||||
(let [page-name (current-page-name)
|
||||
block (ls-api-call! :editor.appendBlockInPage page-name "scalar property owner")
|
||||
uuid (get block "uuid")
|
||||
journal (ls-api-call! :editor.createJournalPage
|
||||
"2026-07-28T12:00:00")
|
||||
journal-id (get journal "id")]
|
||||
(doseq [[property-name property-type valid-value invalid-value]
|
||||
[["sample-number" "number" -12.5 "not-a-number"]
|
||||
["sample-date" "date" journal-id "not-a-date"]]]
|
||||
(ls-api-call! :editor.upsertProperty property-name {:type property-type})
|
||||
(ls-api-call! :editor.upsertBlockProperty uuid property-name valid-value)
|
||||
(is (some? (block-value uuid property-name)))
|
||||
(let [property-row
|
||||
(loc/filter (format "#ls-block-%s .bottom-property-pill" uuid)
|
||||
:has-text property-name)]
|
||||
(w/click
|
||||
(.locator property-row ".bottom-property-content .jtrigger")))
|
||||
(k/press "ControlOrMeta+a")
|
||||
(util/press-seq invalid-value)
|
||||
(k/enter)
|
||||
(is (not= invalid-value (block-value uuid property-name))))
|
||||
(let [property-name "sample-url"
|
||||
initial-value "https://logseq.com"
|
||||
updated-value "https://docs.logseq.com"]
|
||||
(ls-api-call! :editor.upsertProperty property-name {:type "url"})
|
||||
(ls-api-call! :editor.upsertBlockProperty uuid property-name initial-value)
|
||||
(is (= initial-value (block-value uuid property-name)))
|
||||
(let [property-row
|
||||
(loc/filter (format "#ls-block-%s .property-pair" uuid)
|
||||
:has-text property-name)]
|
||||
(w/click
|
||||
(.locator property-row ".property-block-container.jtrigger")))
|
||||
(util/input updated-value)
|
||||
(k/enter)
|
||||
(is (= updated-value (block-value uuid property-name))))
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-is-visible (format "#ls-block-%s" uuid)))))
|
||||
|
||||
(deftest property-type-cardinality-and-checkbox-choice-test
|
||||
(testing "schema controls expose cardinality and allow unused properties to change type"
|
||||
(let [property-name "sample-schema-property"
|
||||
checkbox-property-name "sample-checkbox-choice"
|
||||
page-name (current-page-name)
|
||||
block (ls-api-call! :editor.appendBlockInPage page-name "schema property owner")
|
||||
uuid (get block "uuid")]
|
||||
(ls-api-call! :editor.upsertProperty property-name
|
||||
{:type "default" :cardinality "many"})
|
||||
(ls-api-call! :editor.upsertBlockProperty uuid property-name ["one" "two"])
|
||||
(is (= 2 (count (block-value uuid property-name))))
|
||||
(page/goto-page property-name)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-table-header-cell" :has-text property-name))
|
||||
(w/click
|
||||
(.first
|
||||
(loc/filter ".ls-table-header-cell" :has-text property-name)))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-property-dropdown" :has-text "Multiple values"))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-property-dropdown" :has-text "Property type"))
|
||||
(k/esc)
|
||||
(ls-api-call! :editor.upsertProperty
|
||||
checkbox-property-name
|
||||
{:type "default"})
|
||||
(page/goto-page checkbox-property-name)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-table-header-cell" :has-text checkbox-property-name))
|
||||
(w/click
|
||||
(.first
|
||||
(loc/filter ".ls-table-header-cell" :has-text checkbox-property-name)))
|
||||
(w/click
|
||||
(loc/filter "[role='menuitem']" :has-text "Property type"))
|
||||
(w/click
|
||||
(util/get-by-text "Checkbox" true))
|
||||
(is (= "checkbox"
|
||||
(wait-for-property-type checkbox-property-name "checkbox"))))))
|
||||
|
||||
(deftest property-default-description-position-and-hidden-state-test
|
||||
(testing "property presentation/default settings apply to new objects and hidden values"
|
||||
(let [property-name "sample-configured-property"
|
||||
tag-name "sample configured class"
|
||||
property (ls-api-call! :editor.upsertProperty
|
||||
property-name
|
||||
{:type "default"})
|
||||
property-uuid (get property "uuid")]
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
property-uuid
|
||||
"logseq.property/default-value"
|
||||
"sample default")
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
property-uuid
|
||||
"logseq.property/description"
|
||||
"sample description")
|
||||
(ls-api-call! :editor.createTag
|
||||
tag-name
|
||||
{:tagProperties [{:name property-name}]})
|
||||
(page/new-page "configured object host")
|
||||
(b/new-block "configured object")
|
||||
(util/set-tag tag-name)
|
||||
(util/exit-edit)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".property-pair" :has-text "sample default"))
|
||||
(is (= "sample description"
|
||||
(block-value property-uuid
|
||||
"logseq.property/description")))
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
property-uuid
|
||||
"logseq.property/hide-empty-value"
|
||||
true)
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".property-pair" :has-text property-name)
|
||||
0)
|
||||
(k/press "p")
|
||||
(k/press "a")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".property-pair" :has-text "sample default")))))
|
||||
|
||||
(deftest property-delete-and-bidirectional-refresh-test
|
||||
(testing "bidirectional values refresh both sides and deleting the definition removes usages"
|
||||
(let [property-name "sample bidirectional"
|
||||
owner-tag "SampleOwner"
|
||||
container-page "bidirectional refresh host"
|
||||
owner-class (ls-api-call! :editor.createTag
|
||||
owner-tag
|
||||
{:tagProperties [{:name property-name
|
||||
:schema {:type "node"}}]})
|
||||
property (ls-api-call! :editor.getProperty property-name)]
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
(get property "id")
|
||||
"logseq.property/classes"
|
||||
(get owner-class "id"))
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
(get owner-class "uuid")
|
||||
"logseq.property.class/enable-bidirectional?"
|
||||
true)
|
||||
(ls-api-call! :editor.createPage container-page)
|
||||
(let [owner (ls-api-call! :editor.insertBlock
|
||||
container-page
|
||||
(str "sample owner object #" owner-tag))
|
||||
target (ls-api-call! :editor.createPage "sample target object")]
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
(get owner "uuid") property-name (get target "id"))
|
||||
(page/goto-page "sample target object")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-bidirectional-properties" :has-text "sample owner object"))
|
||||
(ls-api-call! :editor.removeBlockProperty (get owner "uuid") property-name)
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".ls-bidirectional-properties" :has-text "sample owner object")
|
||||
0)
|
||||
(ls-api-call! :editor.removeProperty property-name)
|
||||
(is (nil? (ls-api-call! :editor.getProperty property-name)))))))
|
||||
|
||||
(deftest tag-inheritance-schema-and-object-view-test
|
||||
(testing "class inheritance and schema mutations update object views without losing values"
|
||||
(let [parent-tag-name "SampleParent"
|
||||
child-tag-name "SampleChild"
|
||||
inherited-property "sample-inherited-property"
|
||||
child-property "sample-child-property"
|
||||
parent-tag (ls-api-call! :editor.createTag
|
||||
parent-tag-name
|
||||
{:tagProperties [{:name inherited-property}]})
|
||||
child-tag (ls-api-call! :editor.createTag
|
||||
child-tag-name
|
||||
{:tagProperties [{:name child-property}]})]
|
||||
(ls-api-call! :editor.addTagExtends
|
||||
(get child-tag "id")
|
||||
(get parent-tag "id"))
|
||||
(page/new-page "tag object host")
|
||||
(let [object (ls-api-call! :editor.appendBlockInPage
|
||||
(current-page-name)
|
||||
(str "sample child object #" child-tag-name)
|
||||
{:properties {inherited-property "kept"}})]
|
||||
(page/goto-page parent-tag-name)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body" :has-text "sample child object"))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-table-header-cell" :has-text inherited-property))
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
(get object "uuid") child-property "child value")
|
||||
(page/goto-page child-tag-name)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body" :has-text "child value"))
|
||||
(is (= "kept"
|
||||
(block-value (get object "uuid")
|
||||
inherited-property)))))))
|
||||
|
||||
(deftest tag-template-dynamic-values-test
|
||||
(testing "a tag template is applied once and resolves dynamic date references"
|
||||
(let [tag-name "sample template class"
|
||||
template-page "sample tag template"
|
||||
template-root "sample template root"]
|
||||
(page/new-page template-page)
|
||||
(let [template (ls-api-call! :editor.appendBlockInPage
|
||||
template-page
|
||||
template-root)
|
||||
template-uuid (get template "uuid")
|
||||
template-tag (ls-api-call! :editor.getTag "Template")
|
||||
tag (ls-api-call! :editor.createTag tag-name)]
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
template-uuid
|
||||
"block/tags"
|
||||
(get template-tag "id"))
|
||||
(ls-api-call! :editor.insertBatchBlock
|
||||
template-uuid
|
||||
[{:content "template static child"}
|
||||
{:content "template date <% today %>"}]
|
||||
{:sibling false})
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
template-uuid
|
||||
"logseq.property/template-applied-to"
|
||||
(get tag "id"))
|
||||
(page/new-page "sample templated object")
|
||||
(let [object (ls-api-call! :editor.appendBlockInPage
|
||||
(current-page-name)
|
||||
"templated object")]
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
(get object "uuid")
|
||||
"block/tags"
|
||||
(get tag "id")))
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".ls-page-blocks" :has-text "template static child")
|
||||
1)
|
||||
(assert/assert-is-visible ".page-reference")
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".ls-page-blocks" :has-text "template static child")
|
||||
1)))))
|
||||
|
||||
(deftest checkbox-property-toggle-persists-test
|
||||
(testing "checkbox property toggles immediately and survives refresh"
|
||||
(let [property-name "e2e checkbox"
|
||||
page-name (page/get-page-name)]
|
||||
(ls-api-call! :editor.upsertProperty property-name {:type "checkbox"})
|
||||
(let [block (ls-api-call! :editor.appendBlockInPage
|
||||
page-name
|
||||
"checkbox owner"
|
||||
{:properties {property-name false}})
|
||||
uuid (get block "uuid")
|
||||
checkbox (w/-query
|
||||
(format "#ls-block-%s .bottom-property-pill" uuid))]
|
||||
(w/click (.locator checkbox "button[role='checkbox']"))
|
||||
(assert/assert-is-visible
|
||||
(.locator checkbox "button[role='checkbox'][data-checked]"))
|
||||
(is (true? (block-value uuid property-name)))
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-is-visible
|
||||
(format "#ls-block-%s [role='checkbox'][data-checked]" uuid))))))
|
||||
163
clj-e2e/test/logseq/e2e/cmdk_scroll_basic_test.clj
Normal file
163
clj-e2e/test/logseq/e2e/cmdk_scroll_basic_test.clj
Normal file
@@ -0,0 +1,163 @@
|
||||
(ns logseq.e2e.cmdk-scroll-basic-test
|
||||
"E2E tests for Cmd+K scroll & highlight behavior."
|
||||
(:require
|
||||
[clojure.test :refer [deftest testing is use-fixtures]]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.page :as p]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each fixtures/new-logseq-page fixtures/validate-graph)
|
||||
|
||||
(def ^:private scroll-container ".cp__cmdk .overflow-y-auto")
|
||||
(def ^:private kbd-highlight (str scroll-container " [data-kb-highlighted]"))
|
||||
(def ^:private mouse-item (str scroll-container " .transition-colors.cursor-pointer"))
|
||||
|
||||
(defn- setup-search
|
||||
"Creates `n` pages with `prefix` and `suffix`, opens cmdk and types the `prefix`."
|
||||
([prefix n] (setup-search prefix n ""))
|
||||
([prefix n suffix]
|
||||
[prefix n suffix]
|
||||
(dotimes [i n] (p/new-page (str prefix i suffix)))
|
||||
(util/search prefix)
|
||||
(util/wait-timeout 500)))
|
||||
|
||||
(defn- simulate-mouse-move-into-results!
|
||||
"Dispatches a synthetic mousemove with non-zero movementX/Y on a result item.
|
||||
Playwright's CDP mouse.move does not populate movementX/Y, so we must
|
||||
construct the event in JS and patch those read-only fields."
|
||||
[]
|
||||
(w/eval-js
|
||||
(str "(() => {
|
||||
const c = document.querySelector('" scroll-container "');
|
||||
if (!c) return;
|
||||
const items = c.querySelectorAll('.transition-colors');
|
||||
const target = items[2] || items[0] || c;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const evt = new MouseEvent('mousemove', {
|
||||
bubbles: true, cancelable: true,
|
||||
clientX: rect.x + rect.width / 2,
|
||||
clientY: rect.y + rect.height / 2
|
||||
});
|
||||
Object.defineProperty(evt, 'movementX', {value: 5});
|
||||
Object.defineProperty(evt, 'movementY', {value: 3});
|
||||
target.dispatchEvent(evt);
|
||||
})()")))
|
||||
|
||||
(defn- kbd-highlight-in-viewport?
|
||||
"Returns true when the keyboard-highlighted element is fully within
|
||||
the visible scroll area. Allows ±5 px tolerance for borders/padding."
|
||||
[]
|
||||
(w/eval-js
|
||||
(str "(() => {
|
||||
const el = document.querySelector('" kbd-highlight "');
|
||||
if (!el) return false;
|
||||
const c = document.querySelector('" scroll-container "');
|
||||
const cr = c.getBoundingClientRect();
|
||||
const er = el.getBoundingClientRect();
|
||||
return er.top >= cr.top - 5 && er.bottom <= cr.bottom + 5;
|
||||
})()")))
|
||||
|
||||
(deftest cmdk-keeps-results-visible-while-searching
|
||||
(testing "typing and deleting do not clear the visible results before the debounced search finishes"
|
||||
(let [prefix (str "cmdkflicker" (random-uuid))
|
||||
result-selector (format "[data-testid^='%s']" prefix)]
|
||||
(setup-search prefix 2 "a")
|
||||
(let [result-count (util/count-elements result-selector)]
|
||||
(is (pos? result-count) "initial search results are visible")
|
||||
|
||||
(util/press-seq "a")
|
||||
(is (= result-count (util/count-elements result-selector))
|
||||
"typing keeps the previous results visible")
|
||||
(util/wait-timeout 400)
|
||||
(is (= result-count (util/count-elements result-selector))
|
||||
"matching results remain visible after search settles")
|
||||
|
||||
(k/press "Backspace")
|
||||
(is (= result-count (util/count-elements result-selector))
|
||||
"deleting keeps the previous results visible")
|
||||
(util/wait-timeout 400)
|
||||
(is (= result-count (util/count-elements result-selector))
|
||||
"matching results remain visible after deletion search settles")))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Highlight mode switching
|
||||
;;
|
||||
;; Cycles keyboard → mouse → keyboard → mouse and verifies:
|
||||
;; • keyboard mode: [data-kb-highlighted] = 1, mouse-item count = 0
|
||||
;; • mouse mode: [data-kb-highlighted] = 0, mouse-item count > 0
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest cmdk-highlight-mode-switching
|
||||
(testing "keyboard & mouse highlight mode toggles correctly"
|
||||
(setup-search "cmdkhighlight" 8)
|
||||
|
||||
(dotimes [_ 3] (k/arrow-down))
|
||||
(util/wait-timeout 100)
|
||||
(is (= 1 (util/count-elements kbd-highlight))
|
||||
"keyboard mode → keyboard-highlight = 1")
|
||||
(is (= 0 (util/count-elements mouse-item))
|
||||
"keyboard mode → mouse-mode items = 0")
|
||||
|
||||
(simulate-mouse-move-into-results!)
|
||||
(util/wait-timeout 200)
|
||||
(is (= 0 (util/count-elements kbd-highlight))
|
||||
"mouse mode → keyboard-highlight = 0")
|
||||
(is (pos? (util/count-elements mouse-item))
|
||||
"mouse mode → mouse-mode items > 0")
|
||||
|
||||
(k/arrow-down)
|
||||
(util/wait-timeout 100)
|
||||
(is (= 1 (util/count-elements kbd-highlight))
|
||||
"keyboard mode again → keyboard-highlight = 1")
|
||||
(is (= 0 (util/count-elements mouse-item))
|
||||
"keyboard mode again → mouse-mode items = 0")
|
||||
|
||||
(simulate-mouse-move-into-results!)
|
||||
(util/wait-timeout 200)
|
||||
(is (= 0 (util/count-elements kbd-highlight))
|
||||
"mouse mode again → keyboard-highlight = 0")
|
||||
(is (pos? (util/count-elements mouse-item))
|
||||
"mouse mode again → mouse-mode items > 0")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Lazy-visible keyboard scroll verification
|
||||
;;
|
||||
;; Goal:
|
||||
;; Verify that keyboard navigation scrolls correctly through :nodes items
|
||||
;; that are wrapped in lazy-visible (always active for :nodes group).
|
||||
;; This exercises the data-item-index wrapper-div DOM query path in
|
||||
;; move-highlight, which finds the wrapper even when the inner list-item
|
||||
;; has not yet been mounted by IntersectionObserver.
|
||||
;;
|
||||
;; Layout:
|
||||
;; viewport_height = 65dvh × 720 px (Playwright default) = 468 px
|
||||
;; item_height ≈ 32 px, group_header = 32 px
|
||||
;; Create (32) + nodes header (32) + 30 nodes items (30 × 32 = 960)
|
||||
;; = 1024 px total — significantly exceeds viewport
|
||||
;; Navigating 20 items → top ≈ 32 + 32 + 20×32 = 704 px
|
||||
;; → far below viewport bottom (468 px), deep into lazy territory
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(deftest cmdk-lazy-visible-keyboard-scroll
|
||||
(testing "keyboard navigation scrolls through lazy-visible :nodes items"
|
||||
(setup-search "cmdklazy" 30)
|
||||
|
||||
;; Expand :nodes group — sets :show :more.
|
||||
(k/arrow-down)
|
||||
(util/wait-timeout 100)
|
||||
(k/press "ControlOrMeta+ArrowDown")
|
||||
(util/wait-timeout 300)
|
||||
|
||||
;; Navigate down 20 steps — well past the viewport into lazy items.
|
||||
(k/press (vec (repeat 20 "ArrowDown")) {:delay 30})
|
||||
(util/wait-timeout 300)
|
||||
|
||||
;; Verify keyboard highlight exists.
|
||||
(is (= 1 (util/count-elements kbd-highlight))
|
||||
"keyboard highlight present after navigating through lazy items")
|
||||
|
||||
;; Verify the highlighted element is within the visible scroll area.
|
||||
(is (kbd-highlight-in-viewport?) "highlight is in viewport after navigating through lazy-visible items")))
|
||||
416
clj-e2e/test/logseq/e2e/commands_basic_test.clj
Normal file
416
clj-e2e/test/logseq/e2e/commands_basic_test.clj
Normal file
@@ -0,0 +1,416 @@
|
||||
(ns logseq.e2e.commands-basic-test
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest testing is use-fixtures]]
|
||||
[jsonista.core :as json]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(def ^:private date-picker-day-selector
|
||||
".ui__calendar [role='gridcell'] button, .ui__calendar button[role='gridcell']")
|
||||
|
||||
(defn- focused-date-picker-day
|
||||
[]
|
||||
(json/read-value
|
||||
(w/eval-js
|
||||
(format
|
||||
"(() => {
|
||||
const active = document.activeElement;
|
||||
const dayButton = active?.closest?.(%s);
|
||||
return JSON.stringify({
|
||||
focused: !!dayButton,
|
||||
text: dayButton?.textContent ?? null,
|
||||
label: dayButton?.getAttribute('aria-label') ?? dayButton?.textContent ?? null
|
||||
});
|
||||
})()"
|
||||
(json/write-value-as-string date-picker-day-selector)))
|
||||
json/keyword-keys-object-mapper))
|
||||
|
||||
(defn- assert-date-picker-keyboard-navigation
|
||||
[command]
|
||||
(b/new-block (str command " keyboard test"))
|
||||
(util/input-command command)
|
||||
(w/wait-for date-picker-day-selector)
|
||||
(let [initial (focused-date-picker-day)]
|
||||
(is (:focused initial)
|
||||
(str command " should focus a calendar day when opened."))
|
||||
(k/arrow-right)
|
||||
(let [right (focused-date-picker-day)]
|
||||
(is (:focused right)
|
||||
(str command " should keep calendar focus after ArrowRight."))
|
||||
(is (not= (:label initial) (:label right))
|
||||
(str command " should move focused date with ArrowRight."))
|
||||
(k/arrow-left)
|
||||
(is (= (:label initial) (:label (focused-date-picker-day)))
|
||||
(str command " should move focused date back with ArrowLeft.")))
|
||||
(k/arrow-down)
|
||||
(let [down (focused-date-picker-day)]
|
||||
(is (:focused down)
|
||||
(str command " should keep calendar focus after ArrowDown."))
|
||||
(is (not= (:label initial) (:label down))
|
||||
(str command " should move focused date with ArrowDown."))
|
||||
(k/arrow-up)
|
||||
(is (= (:label initial) (:label (focused-date-picker-day)))
|
||||
(str command " should move focused date back with ArrowUp.")))
|
||||
(k/enter)
|
||||
(if (= command "date picker")
|
||||
(w/wait-for-not-visible ".ui__calendar")
|
||||
(assert/assert-is-visible ".ui__calendar"))))
|
||||
|
||||
(deftest command-trigger-test
|
||||
(testing "/command trigger popup"
|
||||
(b/new-block "b2")
|
||||
(util/press-seq " /")
|
||||
(w/wait-for "a.menu-link.chosen:has-text('Node reference')")
|
||||
(k/backspace)
|
||||
(w/wait-for-not-visible ".ui__popover-content")))
|
||||
|
||||
(deftest page-reference-test
|
||||
(testing "Page reference"
|
||||
(b/new-blocks ["b1" ""])
|
||||
(util/input-command "Node reference")
|
||||
(util/press-seq "Another page")
|
||||
(k/enter)
|
||||
(is (= "[[Another page]]" (util/get-edit-content)))
|
||||
(util/exit-edit)
|
||||
(is (= "Another page" (util/get-text "a.page-ref")))))
|
||||
|
||||
(deftest block-reference-test
|
||||
(testing "Block reference"
|
||||
(b/new-blocks ["block test" ""])
|
||||
(util/input-command "Node reference")
|
||||
(util/press-seq "block test")
|
||||
(util/wait-timeout 300)
|
||||
(k/enter)
|
||||
(is (string/includes? (util/get-edit-content) "[["))
|
||||
(util/exit-edit)
|
||||
(is (= "block test" (util/get-text "a.page-ref")))))
|
||||
|
||||
(deftest link-test
|
||||
(testing "/link"
|
||||
(let [add-logseq-link (fn []
|
||||
(util/press-seq "https://logseq.com")
|
||||
(k/tab)
|
||||
(util/press-seq "Logseq")
|
||||
(k/tab)
|
||||
(k/enter))]
|
||||
(b/new-block "")
|
||||
(util/input-command "link")
|
||||
(add-logseq-link)
|
||||
(is (= "[Logseq](https://logseq.com)" (util/get-edit-content)))
|
||||
(util/press-seq " some content ")
|
||||
(util/input-command "link")
|
||||
(add-logseq-link)
|
||||
(is (= (str "[Logseq](https://logseq.com)"
|
||||
" some content "
|
||||
"[Logseq](https://logseq.com)") (util/get-edit-content))))))
|
||||
|
||||
(deftest link-image-test
|
||||
(testing "/image link"
|
||||
(b/new-block "")
|
||||
(util/input-command "image link")
|
||||
(util/press-seq "https://logseq.com/test.png")
|
||||
(k/tab)
|
||||
(util/press-seq "Logseq")
|
||||
(k/tab)
|
||||
(k/enter)
|
||||
(is (= "" (util/get-edit-content)))))
|
||||
|
||||
(deftest underline-test
|
||||
(testing "/underline"
|
||||
(b/new-block "")
|
||||
(util/input-command "underline")
|
||||
(is (= "<ins></ins>" (util/get-edit-content)))
|
||||
(util/press-seq "test")
|
||||
(is (= "<ins>test</ins>" (util/get-edit-content)))
|
||||
(util/move-cursor-to-end)))
|
||||
|
||||
(deftest code-block-test
|
||||
(testing "/code block"
|
||||
(b/new-block "")
|
||||
(util/input-command "code block")
|
||||
(w/wait-for ".CodeMirror")
|
||||
(util/wait-timeout 100)
|
||||
;; create another block
|
||||
(k/shift+enter)
|
||||
(assert/assert-is-hidden ".ls-page-blocks .block-tags")))
|
||||
|
||||
(deftest math-block-test
|
||||
(testing "/math block"
|
||||
(b/new-block "")
|
||||
(util/input-command "math block")
|
||||
(util/press-seq "1 + 2 = 3")
|
||||
(util/exit-edit)
|
||||
(w/wait-for ".katex")
|
||||
(assert/assert-is-hidden ".ls-page-blocks .block-tags")))
|
||||
|
||||
(deftest quote-test
|
||||
(testing "/quote"
|
||||
(b/new-block "")
|
||||
(util/input-command "quote")
|
||||
(w/wait-for "div[data-node-type='quote']")
|
||||
(assert/assert-is-hidden ".ls-page-blocks .block-tags")))
|
||||
|
||||
(deftest quote-heading-test
|
||||
(testing "quote headings render consistently"
|
||||
(b/new-block "Property quote heading")
|
||||
(util/input-command "quote")
|
||||
(util/input-command "h1")
|
||||
(util/exit-edit)
|
||||
(b/new-block "# Markdown quote heading")
|
||||
(util/input-command "quote")
|
||||
(util/exit-edit)
|
||||
(assert/assert-is-visible
|
||||
"div[data-node-type='quote']:has(h1.block-title-wrap.as-heading:has-text('Property quote heading'))")
|
||||
(assert/assert-is-visible
|
||||
"div[data-node-type='quote']:has(h1.block-title-wrap.as-heading:has-text('Markdown quote heading'))")
|
||||
(b/jump-to-block "Markdown quote heading")
|
||||
(assert/assert-editor-mode)
|
||||
(is (= "Markdown quote heading" (util/get-edit-content)))
|
||||
(util/exit-edit)
|
||||
(b/new-block "Plain quote")
|
||||
(util/input-command "quote")
|
||||
(util/exit-edit)
|
||||
(assert/assert-is-visible
|
||||
"div[data-node-type='quote']:has(span.block-title-wrap:has-text('Plain quote'))")))
|
||||
|
||||
(deftest headings-test
|
||||
(testing "/heading"
|
||||
(dotimes [i 6]
|
||||
(let [heading (str "h" (inc i))
|
||||
text (str heading " test ")]
|
||||
(b/new-block text)
|
||||
(util/input-command heading)
|
||||
(is (= text (util/get-edit-content)))
|
||||
(util/exit-edit)
|
||||
(w/wait-for heading)))))
|
||||
|
||||
(deftest status-test
|
||||
(testing "task status commands"
|
||||
(let [status->icon {"Doing" "InProgress50"
|
||||
"In review" "InReview"
|
||||
"Canceled" "Cancelled"}]
|
||||
(doseq [status ["Backlog" "Todo" "Doing" "In review" "Done" "Canceled"]]
|
||||
(let [text (str status " test ")]
|
||||
(b/new-block text)
|
||||
(util/input-command status)
|
||||
(is (= text (util/get-edit-content)))
|
||||
(util/exit-edit)
|
||||
(k/esc)
|
||||
(w/wait-for (str ".ls-icon-" (get status->icon status status))))))))
|
||||
|
||||
(deftest priority-test
|
||||
(testing "task priority commands"
|
||||
(let [priority->icon {"No priority" "line-dashed"}]
|
||||
(doseq [priority ["No priority" "Low" "Medium" "High" "Urgent"]]
|
||||
(let [text (str priority " test ")]
|
||||
(b/new-block text)
|
||||
(util/input-command priority)
|
||||
(is (= text (util/get-edit-content)))
|
||||
(util/exit-edit)
|
||||
(w/wait-for (str ".ls-icon-" (get priority->icon priority
|
||||
(str "priorityLvl" priority)))))))))
|
||||
|
||||
(deftest scheduled-deadline-test
|
||||
(testing "task scheduled and deadline commands"
|
||||
(doseq [command ["Scheduled" "Deadline"]]
|
||||
(fixtures/create-page)
|
||||
(let [text (str command " test ")]
|
||||
(b/new-block text)
|
||||
(util/input-command command)
|
||||
(k/enter)
|
||||
(assert/assert-editor-mode)
|
||||
(util/exit-edit)
|
||||
(is (= command (util/get-text ".property-k")))
|
||||
(is (= "Today" (util/get-text ".ls-datetime a.page-ref")))))))
|
||||
|
||||
(deftest date-command-keyboard-navigation-test
|
||||
(testing "date commands focus the calendar and support keyboard navigation"
|
||||
(doseq [command ["date picker" "Scheduled" "Deadline"]]
|
||||
(fixtures/create-page)
|
||||
(assert-date-picker-keyboard-navigation command))))
|
||||
|
||||
;; TODO: java "MMMM d, yyyy" vs js "MMM do, yyyy"
|
||||
(deftest date-time-test
|
||||
(testing "date time commands"
|
||||
(util/input-command "today")
|
||||
(let [text (util/get-edit-content)]
|
||||
(and (string/starts-with? text "[[")
|
||||
(string/ends-with? text "]]")))
|
||||
(b/new-block "")
|
||||
(util/input-command "yesterday")
|
||||
(let [text (util/get-edit-content)]
|
||||
(and (string/starts-with? text "[[")
|
||||
(string/ends-with? text "]]")))
|
||||
(b/new-block "")
|
||||
(util/input-command "tomorrow")
|
||||
(let [text (util/get-edit-content)]
|
||||
(and (string/starts-with? text "[[")
|
||||
(string/ends-with? text "]]")))
|
||||
;; FIXME:
|
||||
;; (b/new-block "")
|
||||
;; (util/input-command "time")
|
||||
;; (let [text (util/get-edit-content)
|
||||
;; t (tl/local-now)]
|
||||
;; (is (= text (str (t/hour t) ":" (t/minute t)))))
|
||||
(b/new-block "")
|
||||
(util/input-command "date picker")
|
||||
(let [text (util/get-edit-content)]
|
||||
(and (string/starts-with? text "[[")
|
||||
(string/ends-with? text "]]")))))
|
||||
|
||||
(deftest number-list-test
|
||||
(testing "number list commands"
|
||||
(util/input-command "number list")
|
||||
(b/new-blocks ["a" "b" "c"])
|
||||
(assert/assert-have-count "span.typed-list" 3)
|
||||
(is (= ["1." "2." "3."] (w/all-text-contents "span.typed-list")))
|
||||
;; double `enter` convert the next block to bullet block
|
||||
(k/enter)
|
||||
(assert/assert-have-count "span.typed-list" 4)
|
||||
(util/wait-timeout 60)
|
||||
(k/enter)
|
||||
(assert/assert-have-count "span.typed-list" 3)
|
||||
(is (= ["1." "2." "3."] (w/all-text-contents "span.typed-list")))))
|
||||
|
||||
(deftest number-children-test
|
||||
(testing "number children commands"
|
||||
(b/new-blocks ["a" "a1" "a2" "a3" "b"])
|
||||
(k/arrow-up)
|
||||
(w/wait-for "textarea:text('a3')")
|
||||
(util/repeat-keyboard 3 "Shift+ArrowUp")
|
||||
(k/tab)
|
||||
(b/jump-to-block "a")
|
||||
(util/input-command "number children")
|
||||
(assert/assert-have-count "span.typed-list" 3)
|
||||
(is (= ["1." "2." "3."] (w/all-text-contents "span.typed-list")))))
|
||||
|
||||
(deftest query-test
|
||||
(testing "query"
|
||||
(b/new-blocks ["[[foo]] block" "[[foo]] another" ""])
|
||||
(util/input-command "query")
|
||||
(let [btn (util/-query-last "button:text('filter')")]
|
||||
(w/click btn)
|
||||
(util/input "page reference")
|
||||
(w/click "a.menu-link:has-text('page reference')")
|
||||
(w/click (first (w/query "a.menu-link:has-text('foo')")))
|
||||
(assert/assert-is-visible "div:text('Live query (2)')"))))
|
||||
|
||||
(deftest query-view-membership-updates-live
|
||||
(testing "a mounted query view inserts and removes matching rows"
|
||||
(let [tag "live-query-membership"
|
||||
candidate-title "query membership candidate"]
|
||||
(b/new-blocks [(format "[[%s]] query seed" tag)
|
||||
candidate-title
|
||||
""])
|
||||
(let [candidate-uuid (.getAttribute
|
||||
(.first
|
||||
(w/-query
|
||||
(format ".ls-block[data-block-title='%s']"
|
||||
candidate-title)))
|
||||
"blockid")
|
||||
candidate-row (format ".custom-query-results :text('%s')"
|
||||
candidate-title)]
|
||||
(is (string? candidate-uuid))
|
||||
(util/input-command "query")
|
||||
(w/click (util/-query-last "button:text('filter')"))
|
||||
(util/input "page reference")
|
||||
(w/click "a.menu-link:has-text('page reference')")
|
||||
(w/click (first (w/query (format "a.menu-link:has-text('%s')" tag))))
|
||||
(w/wait-for "div:text('Live query (1)')")
|
||||
|
||||
(ls-api-call! :editor.updateBlock
|
||||
candidate-uuid
|
||||
(format "[[%s]] %s" tag candidate-title))
|
||||
(w/wait-for "div:text('Live query (2)')")
|
||||
(w/wait-for candidate-row)
|
||||
|
||||
(ls-api-call! :editor.updateBlock candidate-uuid candidate-title)
|
||||
(w/wait-for "div:text('Live query (1)')")
|
||||
(w/wait-for-not-visible candidate-row)))))
|
||||
|
||||
(deftest advanced-query-test
|
||||
(testing "query"
|
||||
(b/new-blocks ["[[bar]] block" "[[bar]] another" ""])
|
||||
(util/input-command "advanced query")
|
||||
(w/click ".ls-query-setting")
|
||||
(w/click (.first (w/-query "pre.CodeMirror-line")))
|
||||
(util/input "{:query [:find (pull ?b [*])
|
||||
:where [?b :block/refs ?r]
|
||||
[?r :block/title \"bar\"]]}")
|
||||
(k/esc)
|
||||
(is (some? (w/find-one-by-text "div" "Live query (2)")))))
|
||||
|
||||
(deftest calculator-test
|
||||
(testing "calculator"
|
||||
(b/new-block "")
|
||||
(util/input-command "calculator")
|
||||
(util/input "1 + 2")
|
||||
(w/wait-for "div.extensions__code-calc-output-line")
|
||||
(is (= "3" (util/get-text "div.extensions__code-calc-output-line")))
|
||||
(assert/assert-is-hidden ".ls-page-blocks .block-tags")))
|
||||
|
||||
(deftest template-test
|
||||
(testing "template"
|
||||
(b/new-block "template 1")
|
||||
(util/set-tag "Template" :hidden? true)
|
||||
(b/new-blocks ["block 1" "block 2" "block 3" "test"])
|
||||
(k/arrow-up)
|
||||
(w/wait-for "textarea:text('block 3')")
|
||||
(util/repeat-keyboard 3 "Shift+ArrowUp")
|
||||
(k/tab)
|
||||
(b/jump-to-block "test")
|
||||
(util/input-command "template")
|
||||
(util/input "template 1")
|
||||
(w/wait-for "a.menu-link.chosen:has-text('template 1')")
|
||||
(k/enter)
|
||||
(doseq [text ["block 1" "block 2" "block 3"]]
|
||||
(assert/assert-have-count
|
||||
(loc/or (format ".ls-block .block-title-wrap:text('%s')" text)
|
||||
(format ".ls-block textarea:text('%s')" text))
|
||||
2))))
|
||||
|
||||
(deftest embed-html-test
|
||||
(testing "embed html"
|
||||
(b/new-block "")
|
||||
(util/input-command "embed html")
|
||||
(util/press-seq "<div id=\"embed-test\">test</div>")
|
||||
(util/exit-edit)
|
||||
(is (= "test" (util/get-text "#embed-test")))))
|
||||
|
||||
(deftest embed-video-test
|
||||
(testing "embed video"
|
||||
(b/new-block "")
|
||||
(util/input-command "embed video")
|
||||
(util/press-seq "https://www.youtube.com/watch?v=7xTGNNLPyMI")
|
||||
(util/exit-edit)
|
||||
(w/wait-for "iframe")))
|
||||
|
||||
(deftest embed-tweet-test
|
||||
(testing "embed tweet"
|
||||
(b/new-block "")
|
||||
(util/input-command "embed tweet")
|
||||
(util/press-seq "https://x.com/logseq/status/1784914564083314839")
|
||||
(util/exit-edit)
|
||||
(w/wait-for "iframe")))
|
||||
|
||||
(deftest cloze-test
|
||||
(testing "cloze"
|
||||
(b/new-block "")
|
||||
(util/input-command "cloze")
|
||||
(util/press-seq "hidden answer")
|
||||
(util/exit-edit)
|
||||
(w/click "span.cloze")
|
||||
(w/wait-for "span.cloze-revealed")))
|
||||
70
clj-e2e/test/logseq/e2e/custom_report.clj
Normal file
70
clj-e2e/test/logseq/e2e/custom_report.clj
Normal file
@@ -0,0 +1,70 @@
|
||||
(ns logseq.e2e.custom-report
|
||||
(:require [clojure.pprint :as pp]
|
||||
[clojure.stacktrace :as stack]
|
||||
[clojure.string :as string]
|
||||
[clojure.test :as t]
|
||||
[logseq.e2e.playwright-page :as pw-page])
|
||||
(:import (com.microsoft.playwright Page$ScreenshotOptions)))
|
||||
|
||||
(def ^:dynamic *pw-contexts*
|
||||
"Set of pw-contexts.
|
||||
record all playwright contexts in this dynamic var"
|
||||
nil)
|
||||
|
||||
(def ^:dynamic *pw-page->console-logs* nil)
|
||||
|
||||
(def ^:dynamic *preserve-graph* nil)
|
||||
|
||||
(defn screenshot
|
||||
[page test-name]
|
||||
(println :screenshot test-name)
|
||||
(.screenshot
|
||||
page
|
||||
(-> (Page$ScreenshotOptions.)
|
||||
(.setPath (java.nio.file.Paths/get "e2e-dump/"
|
||||
(into-array [(format "./screenshot-%s-%s.png" test-name (System/currentTimeMillis))]))))))
|
||||
|
||||
(defn- collect-info-when-error-or-failed
|
||||
[]
|
||||
;; screenshot for all pw pages when :error
|
||||
(when-let [all-contexts (seq *pw-contexts*)]
|
||||
(doseq [page (mapcat pw-page/get-pages all-contexts)]
|
||||
(screenshot page (string/join "-" (map (comp str :name meta) t/*testing-vars*)))))
|
||||
|
||||
;; dump console logs
|
||||
(when-let [pw-page->console-logs (some-> *pw-page->console-logs* deref)]
|
||||
(doseq [[_pw-page logs] pw-page->console-logs]
|
||||
(spit (format "e2e-dump/console-logs-%s.txt" (System/currentTimeMillis))
|
||||
(with-out-str (pp/pprint logs)))))
|
||||
|
||||
(when (some? *preserve-graph*)
|
||||
(set! *preserve-graph* true)))
|
||||
|
||||
(defmethod t/report :error
|
||||
[m]
|
||||
;; copy from default impl
|
||||
(t/with-test-out
|
||||
(t/inc-report-counter :error)
|
||||
(println "\nERROR in" (t/testing-vars-str m))
|
||||
(when (seq t/*testing-contexts*) (println (t/testing-contexts-str)))
|
||||
(when-let [message (:message m)] (println message))
|
||||
(println "expected:" (pr-str (:expected m)))
|
||||
(print " actual: ")
|
||||
(let [actual (:actual m)]
|
||||
(if (instance? Throwable actual)
|
||||
(stack/print-cause-trace actual t/*stack-trace-depth*)
|
||||
(prn actual))))
|
||||
|
||||
(collect-info-when-error-or-failed))
|
||||
|
||||
(defmethod t/report :fail
|
||||
[m]
|
||||
(t/with-test-out
|
||||
(t/inc-report-counter :fail)
|
||||
(println "\nFAIL in" (t/testing-vars-str m))
|
||||
(when (seq t/*testing-contexts*) (println (t/testing-contexts-str)))
|
||||
(when-let [message (:message m)] (println message))
|
||||
(println "expected:" (pr-str (:expected m)))
|
||||
(println " actual:" (pr-str (:actual m))))
|
||||
|
||||
(collect-info-when-error-or-failed))
|
||||
1627
clj-e2e/test/logseq/e2e/editor_basic_test.clj
Normal file
1627
clj-e2e/test/logseq/e2e/editor_basic_test.clj
Normal file
File diff suppressed because it is too large
Load Diff
67
clj-e2e/test/logseq/e2e/export_basic_test.clj
Normal file
67
clj-e2e/test/logseq/e2e/export_basic_test.clj
Normal file
@@ -0,0 +1,67 @@
|
||||
(ns logseq.e2e.export-basic-test
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w])
|
||||
(:import
|
||||
(com.microsoft.playwright Page$WaitForDownloadOptions)
|
||||
(java.nio.file Files)
|
||||
(java.util.zip ZipFile)))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each fixtures/new-logseq-page fixtures/validate-graph)
|
||||
|
||||
(defn- open-export!
|
||||
[]
|
||||
(util/double-esc)
|
||||
(w/click ".toolbar-dots-btn")
|
||||
(w/click (loc/filter "[role='menuitem']" :has-text "Export graph"))
|
||||
(w/wait-for ".export"))
|
||||
|
||||
(defn- download!
|
||||
([label]
|
||||
(.waitForDownload
|
||||
(w/get-page)
|
||||
(reify Runnable
|
||||
(run [_]
|
||||
(w/click (loc/filter ".export a" :has-text label))))))
|
||||
([label timeout]
|
||||
(.waitForDownload
|
||||
(w/get-page)
|
||||
(-> (Page$WaitForDownloadOptions.)
|
||||
(.setTimeout timeout))
|
||||
(reify Runnable
|
||||
(run [_]
|
||||
(w/click (loc/filter ".export a" :has-text label)))))))
|
||||
|
||||
(defn- nonempty-download?
|
||||
[download]
|
||||
(and (not (string/blank? (.suggestedFilename download)))
|
||||
(pos? (Files/size (.path download)))))
|
||||
|
||||
(deftest graph-export-downloads-browser-artifacts-test
|
||||
(testing "browser graph export produces nonempty DB, zip, EDN, Markdown and transit files"
|
||||
(b/new-blocks ["export root" "export child"])
|
||||
(util/set-tag "export-tag")
|
||||
(b/indent)
|
||||
(open-export!)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".export a" :has-text "Export SQLite DB"))
|
||||
(doseq [label ["Export EDN file"
|
||||
"Export as standard Markdown"
|
||||
"Export debug transit file"]]
|
||||
(let [download (download! label)]
|
||||
(is (nonempty-download? download) label)))
|
||||
(let [download (download! "Export both SQLite DB and assets" 60000)
|
||||
path (.path download)]
|
||||
(is (nonempty-download? download))
|
||||
(with-open [zip (ZipFile. (.toFile path))]
|
||||
(let [entries (map #(.getName %) (enumeration-seq (.entries zip)))]
|
||||
(is (some #(string/ends-with? % ".sqlite") entries))
|
||||
(is (every? #(not (string/blank? %)) entries)))))
|
||||
(assert/assert-is-hidden ".ui__loading, .loading-graph")))
|
||||
169
clj-e2e/test/logseq/e2e/fixtures.clj
Normal file
169
clj-e2e/test/logseq/e2e/fixtures.clj
Normal file
@@ -0,0 +1,169 @@
|
||||
(ns logseq.e2e.fixtures
|
||||
(:require [com.climate.claypoole :as cp]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.config :as config]
|
||||
[logseq.e2e.const :refer [*page1 *page2 *graph-name*]]
|
||||
[logseq.e2e.custom-report :as custom-report]
|
||||
[logseq.e2e.graph :as graph]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.playwright-page :as pw-page]
|
||||
[logseq.e2e.rtc :as rtc]
|
||||
[logseq.e2e.settings :as settings]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
;; TODO: save trace
|
||||
;; TODO: parallel support
|
||||
(defn open-page
|
||||
[f & {:keys [headless port]}]
|
||||
(w/with-page-open
|
||||
(w/make-page {:headless (or headless @config/*headless)
|
||||
:persistent false
|
||||
:slow-mo @config/*slow-mo})
|
||||
(w/grant-permissions :clipboard-write :clipboard-read)
|
||||
(binding [custom-report/*pw-contexts* #{(.context (w/get-page))}
|
||||
custom-report/*pw-page->console-logs* (atom {})]
|
||||
(settings/install-init-script! (.context (w/get-page)))
|
||||
(w/grant-permissions :clipboard-write :clipboard-read)
|
||||
(w/navigate (pw-page/get-test-url port))
|
||||
(settings/developer-mode)
|
||||
(settings/refresh-test-env!)
|
||||
(let [p (w/get-page)]
|
||||
(.onConsoleMessage p (fn [msg]
|
||||
(when custom-report/*pw-page->console-logs*
|
||||
(swap! custom-report/*pw-page->console-logs* update p conj (.text msg))))))
|
||||
(f))))
|
||||
|
||||
(defn open-2-pages
|
||||
"Use `*page1` and `*page2` in `f`"
|
||||
[f & {:keys [headless port]}]
|
||||
(let [headless (or headless @config/*headless)
|
||||
page-opts {:headless headless
|
||||
:persistent false
|
||||
:slow-mo @config/*slow-mo}
|
||||
p1 (w/make-page page-opts)
|
||||
p2 (w/make-page page-opts)]
|
||||
(run! #(settings/install-init-script! (.context @%)) [p1 p2])
|
||||
(reset! *page1 p1)
|
||||
(reset! *page2 p2)
|
||||
(binding [custom-report/*pw-contexts* (set [(.context @p1) (.context @p2)])
|
||||
custom-report/*pw-page->console-logs* (atom {})
|
||||
w/*page* (delay (throw (ex-info "Don't use *page*, use *page1* and *page2* instead" {})))]
|
||||
(run!
|
||||
#(w/with-page %
|
||||
(w/grant-permissions :clipboard-write :clipboard-read)
|
||||
(w/navigate (pw-page/get-test-url (or port @config/*port)))
|
||||
(settings/developer-mode)
|
||||
(settings/refresh-test-env!)
|
||||
(let [p (w/get-page)]
|
||||
(.onConsoleMessage
|
||||
p
|
||||
(fn [msg]
|
||||
(when custom-report/*pw-page->console-logs*
|
||||
(swap! custom-report/*pw-page->console-logs* update p conj (.text msg)))))))
|
||||
[p1 p2])
|
||||
(f))
|
||||
|
||||
;; use with-page-open to release resources
|
||||
(w/with-page-open p1)
|
||||
(w/with-page-open p2)
|
||||
(reset! *page1 nil)
|
||||
(reset! *page2 nil)))
|
||||
|
||||
(def ^:dynamic *pw-ctx* nil)
|
||||
(defn open-new-context
|
||||
"create a new playwright-context in `*pw-ctx*`"
|
||||
[f]
|
||||
(let [page-opts {:headless @config/*headless
|
||||
:persistent false
|
||||
:slow-mo @config/*slow-mo}
|
||||
p @(w/make-page page-opts)
|
||||
ctx (.newContext (.browser (.context p)))]
|
||||
;; context for p is no longer needed
|
||||
(.close (.context p))
|
||||
(w/with-page-open p) ; use with-page-open to close playwright instance
|
||||
(binding [custom-report/*pw-contexts* #{ctx}
|
||||
*pw-ctx* ctx]
|
||||
(settings/install-init-script! ctx)
|
||||
(f)
|
||||
(.close (.browser *pw-ctx*)))))
|
||||
|
||||
(defonce *page-number (atom 0))
|
||||
|
||||
(defn create-page
|
||||
[& [page-name]]
|
||||
(let [page-name (or page-name (str "page " (swap! *page-number inc)))]
|
||||
(page/new-page page-name)
|
||||
page-name))
|
||||
|
||||
(defn new-logseq-page
|
||||
[f]
|
||||
(when (w/visible? ".cp__right-sidebar.open")
|
||||
(w/click ".toggle-right-sidebar")
|
||||
(w/wait-for-not-visible ".cp__right-sidebar.open"))
|
||||
(w/eval-js
|
||||
"() => {
|
||||
const url = new URL(location.href);
|
||||
url.searchParams.delete('virtualized');
|
||||
history.replaceState(null, '', url.pathname + url.search + url.hash);
|
||||
}")
|
||||
(create-page)
|
||||
(f))
|
||||
|
||||
(defn new-logseq-page-in-rtc*
|
||||
"create a logseq page and switch to this page on both `*page1` and `*page2`"
|
||||
[& [page-name]]
|
||||
(let [*page-name (atom nil)
|
||||
{:keys [_local-tx remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(reset! *page-name (create-page page-name))))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx)
|
||||
(page/goto-page @*page-name))))
|
||||
|
||||
(defn new-logseq-page-in-rtc
|
||||
[f]
|
||||
(new-logseq-page-in-rtc*)
|
||||
(f))
|
||||
|
||||
(defn validate-graph
|
||||
[f]
|
||||
(f)
|
||||
(if (and @*page1 @*page2)
|
||||
(doseq [p [@*page1 @*page2]]
|
||||
(w/with-page p
|
||||
(graph/validate-graph)))
|
||||
|
||||
(graph/validate-graph)))
|
||||
|
||||
(def ^:private formatter (java.time.format.DateTimeFormatter/ofPattern "yyyy-MM-dd'T'HH-mm-ss"))
|
||||
(defn- inst-string
|
||||
[inst]
|
||||
(.format formatter (.atZone inst (java.time.ZoneId/of "UTC"))))
|
||||
|
||||
(defn prepare-rtc-graph-fixture
|
||||
"open 2 app instances, add a rtc graph, check this graph available on other instance"
|
||||
[graph-name-prefix f]
|
||||
(let [graph-name (str graph-name-prefix "-" (inst-string (java.time.Instant/now)))]
|
||||
(cp/prun!
|
||||
2
|
||||
#(w/with-page %
|
||||
(settings/developer-mode)
|
||||
(settings/refresh-test-env!)
|
||||
(util/login-test-account))
|
||||
[@*page1 @*page2])
|
||||
(w/with-page @*page1
|
||||
(graph/new-graph graph-name true false))
|
||||
(w/with-page @*page2
|
||||
(graph/wait-for-remote-graph graph-name)
|
||||
(graph/switch-graph graph-name true true))
|
||||
|
||||
(binding [custom-report/*preserve-graph* false
|
||||
*graph-name* graph-name]
|
||||
(f)
|
||||
;; cleanup
|
||||
(if custom-report/*preserve-graph*
|
||||
(println "Don't remove graph: " graph-name)
|
||||
(w/with-page @*page2
|
||||
(graph/remove-remote-graph graph-name))))))
|
||||
84
clj-e2e/test/logseq/e2e/flashcards_basic_test.clj
Normal file
84
clj-e2e/test/logseq/e2e/flashcards_basic_test.clj
Normal file
@@ -0,0 +1,84 @@
|
||||
(ns logseq.e2e.flashcards-basic-test
|
||||
(:require [clojure.test :refer [deftest testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/validate-graph)
|
||||
|
||||
(defn- open-flashcards
|
||||
[]
|
||||
(util/double-esc)
|
||||
(when-not (w/visible? ".flashcards-nav")
|
||||
(w/click "#left-menu")
|
||||
(w/wait-for ".flashcards-nav"))
|
||||
(w/eval-js "selector => document.querySelector(selector)?.click()" ".flashcards-nav a")
|
||||
(assert/assert-is-visible "#cards-modal"))
|
||||
|
||||
(defn- select-cards-option
|
||||
[label]
|
||||
(w/click "#cards-modal [role='combobox']")
|
||||
(w/click (loc/filter "[role='option']" :has-text label)))
|
||||
|
||||
(defn- click-flashcards-plus
|
||||
[]
|
||||
(w/click "#ls-cards-add"))
|
||||
|
||||
(defn- setup-flashcards-data!
|
||||
[{:keys [page-name tag-a tag-b card-a card-b query-a query-b]}]
|
||||
(ls-api-call! :editor.appendBlockInPage page-name (str card-a " #Card #" tag-a))
|
||||
(ls-api-call! :editor.appendBlockInPage page-name (str card-b " #Card #" tag-b))
|
||||
(let [cards (ls-api-call! :editor.getTag "logseq.class/Cards")
|
||||
cards-id (get cards "id")
|
||||
cards-a (ls-api-call! :editor.appendBlockInPage page-name "Cards A"
|
||||
{:properties {:block/tags #{cards-id}}})
|
||||
cards-b (ls-api-call! :editor.appendBlockInPage page-name "Cards B"
|
||||
{:properties {:block/tags #{cards-id}}})
|
||||
query-a-id (get cards-a ":logseq.property/query")
|
||||
query-b-id (get cards-b ":logseq.property/query")]
|
||||
(ls-api-call! :editor.updateBlock query-a-id query-a)
|
||||
(ls-api-call! :editor.updateBlock query-b-id query-b)))
|
||||
|
||||
(deftest flashcards-plus-and-switching-test
|
||||
(testing "create #Cards blocks from flashcards dialog and switch card sets"
|
||||
(let [tag-a "fc-tag-a"
|
||||
tag-b "fc-tag-b"
|
||||
card-a "Card A"
|
||||
card-b "Card B"
|
||||
query-a (str "[[" tag-a "]]")
|
||||
query-b (str "[[" tag-b "]]")]
|
||||
(util/goto-journals)
|
||||
(let [page (ls-api-call! :editor.getCurrentPage)
|
||||
page-name (get page "name")]
|
||||
(setup-flashcards-data!
|
||||
{:page-name page-name
|
||||
:tag-a tag-a
|
||||
:tag-b tag-b
|
||||
:card-a card-a
|
||||
:card-b card-b
|
||||
:query-a query-a
|
||||
:query-b query-b}))
|
||||
|
||||
(open-flashcards)
|
||||
(click-flashcards-plus)
|
||||
(w/wait-for ".ls-block .tag:has-text('Cards')")
|
||||
|
||||
(open-flashcards)
|
||||
(select-cards-option "Cards A")
|
||||
(assert/assert-is-visible (format "#cards-modal .ls-card :text('%s')" card-a))
|
||||
(assert/assert-have-count (format "#cards-modal .ls-card :text('%s')" card-b) 0)
|
||||
(assert/assert-is-visible (loc/filter "#cards-modal .text-sm.opacity-50" :has-text "1/1"))
|
||||
|
||||
(select-cards-option "Cards B")
|
||||
(assert/assert-is-visible (format "#cards-modal .ls-card :text('%s')" card-b))
|
||||
(assert/assert-have-count (format "#cards-modal .ls-card :text('%s')" card-a) 0)
|
||||
(assert/assert-is-visible (loc/filter "#cards-modal .text-sm.opacity-50" :has-text "1/1"))
|
||||
|
||||
(select-cards-option "All cards")
|
||||
(assert/assert-is-visible (loc/filter "#cards-modal .text-sm.opacity-50" :has-text "1/2")))))
|
||||
259
clj-e2e/test/logseq/e2e/graph_navigation_basic_test.clj
Normal file
259
clj-e2e/test/logseq/e2e/graph_navigation_basic_test.clj
Normal file
@@ -0,0 +1,259 @@
|
||||
(ns logseq.e2e.graph-navigation-basic-test
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.graph :as graph]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each fixtures/new-logseq-page fixtures/validate-graph)
|
||||
|
||||
(deftest graph-empty-state-and-name-validation-test
|
||||
(testing "graph creation exposes an actionable empty state and rejects invalid/duplicate names"
|
||||
(let [graph-name (str "sample-valid-graph-" (random-uuid))]
|
||||
(graph/goto-all-graphs)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "#main-content-container" :has-text "Create a new graph"))
|
||||
(util/search-and-click "Add a DB graph")
|
||||
(let [input (w/-query "input[placeholder='your graph name']")]
|
||||
(w/fill input "")
|
||||
(w/click "button:text('Submit')")
|
||||
(assert/assert-is-visible ".new-graph")
|
||||
(doseq [invalid-name ["bad/name" "bad\\name"]]
|
||||
(w/fill input invalid-name)
|
||||
(w/click "button:text('Submit')")
|
||||
(assert/assert-is-visible
|
||||
".ui__toast.warning:has-text(\"Graph name can't contain\")")
|
||||
(w/click (.last (w/-query ".ui__toast.warning button"))))
|
||||
(w/fill input graph-name)
|
||||
(w/click "button:text('Submit')")
|
||||
(assert/assert-graph-loaded?)
|
||||
(graph/goto-all-graphs)
|
||||
(util/search-and-click "Add a DB graph")
|
||||
(w/fill "input[placeholder='your graph name']" graph-name)
|
||||
(w/click "button:text('Submit')")
|
||||
(assert/assert-is-visible
|
||||
".ui__toast.error:has-text('already exists')")
|
||||
(k/esc)
|
||||
(graph/switch-graph graph-name false false)))))
|
||||
|
||||
(deftest graph-refresh-and-browser-history-test
|
||||
(testing "refresh and back/forward restore current graph/page without duplicate history"
|
||||
(let [page-a "graph history a"
|
||||
page-b "graph history b"]
|
||||
(page/new-page page-a)
|
||||
(b/new-block "history a content")
|
||||
(util/exit-edit)
|
||||
(page/new-page page-b)
|
||||
(b/new-block "history b content")
|
||||
(util/exit-edit)
|
||||
(.goBack (w/get-page))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "#main-content-container" :has-text "history a content"))
|
||||
(.goForward (w/get-page))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "#main-content-container" :has-text "history b content"))
|
||||
(util/refresh-until-graph-loaded)
|
||||
(is (= page-b (page/get-page-name)))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "#main-content-container" :has-text "history b content")))))
|
||||
|
||||
(deftest direct-page-and-block-route-test
|
||||
(testing "direct page/block URLs load exact worker entities and missing routes recover"
|
||||
(let [suffix (str (random-uuid))
|
||||
page-name (str "direct-route-page-" suffix)
|
||||
block-content (str "direct route block " suffix)]
|
||||
(page/new-page page-name)
|
||||
(b/new-block block-content)
|
||||
(let [block-uuid (.getAttribute
|
||||
(util/get-edit-block-container)
|
||||
"blockid")]
|
||||
(util/exit-edit)
|
||||
(util/refresh-until-graph-loaded)
|
||||
(let [page-uuid (get (ls-api-call! :editor.getPage page-name) "uuid")
|
||||
block (ls-api-call! :editor.getBlock block-uuid)
|
||||
current-url (.url (w/get-page))
|
||||
base (-> current-url
|
||||
(string/replace #"[?#].*$" "")
|
||||
(string/replace #"/$" ""))
|
||||
graph-id (some-> (re-find #"[?&]graph-id=([^&]+)" current-url)
|
||||
second)
|
||||
direct-url (fn [kind uuid]
|
||||
(str base
|
||||
"/#/"
|
||||
kind
|
||||
"/"
|
||||
uuid
|
||||
"?graph-id="
|
||||
graph-id))
|
||||
navigate-direct! (fn [url]
|
||||
(w/navigate "about:blank")
|
||||
(w/navigate url))]
|
||||
(is (= block-content (get block "content")))
|
||||
(is (some? block-uuid))
|
||||
(is (some? graph-id))
|
||||
(navigate-direct! (direct-url "page" page-uuid))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "[data-testid='page title']" :has-text page-name))
|
||||
(is (= page-name (page/get-page-name)))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "#main-content-container" :has-text block-content))
|
||||
(is (= block-content
|
||||
(get (ls-api-call! :editor.getBlock block-uuid) "content")))
|
||||
(navigate-direct! (direct-url "block" block-uuid))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "#main-content-container" :has-text block-content))
|
||||
(navigate-direct! (direct-url "page" (random-uuid)))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "#main-content-container" :has-text "Page not found"))
|
||||
(util/goto-journals)
|
||||
(assert/assert-is-visible "#journals"))))))
|
||||
|
||||
(deftest local-graph-delete-and-list-metadata-test
|
||||
(testing "graph list metadata is current and local deletion cannot leak its route/data"
|
||||
(let [graph-name (str "graph-delete-" (random-uuid))]
|
||||
(graph/new-graph graph-name false)
|
||||
(page/new-page "local graph deletion page")
|
||||
(b/new-block "local graph deletion content")
|
||||
(graph/goto-all-graphs)
|
||||
(let [card (w/-query (format "div[data-testid='logseq_db_%s']" graph-name))]
|
||||
(assert/assert-is-visible card)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter card :has-text "Last opened at:"))
|
||||
(w/click (.locator card ".graph-action-btn"))
|
||||
(w/click ".delete-local-graph-menu-item")
|
||||
(w/click "div[role='alertdialog'] button:text('Confirm')")
|
||||
(assert/assert-have-count card 0))
|
||||
(assert/assert-have-count
|
||||
(loc/filter "#main-content-container" :has-text "local graph deletion content")
|
||||
0)
|
||||
(graph/switch-graph "Demo" false false)
|
||||
(page/new-page "post deletion validation")
|
||||
(util/exit-edit)
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-is-visible "#search-button"))))
|
||||
|
||||
(deftest default-home-route-test
|
||||
(testing "configured default home opens on startup and invalid home falls back explicitly"
|
||||
(let [set-default-home!
|
||||
(fn [page-name]
|
||||
(ls-api-call! :app.setCurrentGraphConfigs
|
||||
{"default-home" (cond-> {}
|
||||
page-name
|
||||
(assoc "page" page-name))})
|
||||
(loop [remaining 40]
|
||||
(let [stored-page
|
||||
(ls-api-call! :app.getCurrentGraphConfigs
|
||||
"default-home"
|
||||
"page")]
|
||||
(when-not (= page-name stored-page)
|
||||
(when (zero? remaining)
|
||||
(throw (ex-info "Default home config did not persist"
|
||||
{:expected page-name
|
||||
:actual stored-page})))
|
||||
(util/wait-timeout 250)
|
||||
(recur (dec remaining))))))]
|
||||
(page/new-page "sample default home")
|
||||
(b/new-block "default home content")
|
||||
(util/exit-edit)
|
||||
(let [current-url (.url (w/get-page))
|
||||
base (-> current-url
|
||||
(string/replace #"[?#].*$" "")
|
||||
(string/replace #"/$" ""))
|
||||
graph-id (some-> (re-find #"[?&]graph-id=([^&]+)" current-url)
|
||||
second)
|
||||
home-url (str base "/#/?graph-id=" graph-id)
|
||||
navigate-home! (fn []
|
||||
(w/navigate "about:blank")
|
||||
(w/navigate home-url))]
|
||||
(is (some? graph-id))
|
||||
(set-default-home! "sample default home")
|
||||
(navigate-home!)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "[data-testid='page title']"
|
||||
:has-text "sample default home"))
|
||||
(is (= "sample default home" (page/get-page-name)))
|
||||
(set-default-home! "missing sample home")
|
||||
(navigate-home!)
|
||||
(assert/assert-is-visible "#journals")
|
||||
(assert/assert-is-hidden ".loading-graph")
|
||||
(set-default-home! nil)
|
||||
(page/new-page "post default home validation")
|
||||
(util/exit-edit)
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-is-visible "#search-button")))))
|
||||
|
||||
(deftest graph-view-mode-settings-test
|
||||
(testing "Graph view modes and settings rebuild the canvas"
|
||||
(doseq [title ["graph view alpha" "graph view beta" "graph view gamma"]]
|
||||
(page/new-page title)
|
||||
(b/new-block "[[graph view alpha]]")
|
||||
(util/set-tag title)
|
||||
(util/exit-edit))
|
||||
(util/search-and-click "Go to graph view")
|
||||
(assert/assert-is-visible "#global-graph.graph-root")
|
||||
(assert/assert-is-visible
|
||||
"[role='application'][aria-label='Graph canvas']")
|
||||
(w/click ".graph-settings-toggle")
|
||||
(w/click (loc/filter ".graph-mode-tab" :has-text "Tags"))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".graph-mode-tab[aria-selected='true']" :has-text "Tags"))
|
||||
(w/click (loc/filter ".graph-mode-tab" :has-text "All pages"))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".graph-mode-tab[aria-selected='true']" :has-text "All pages"))
|
||||
(w/refresh)
|
||||
(assert/assert-is-visible "#global-graph.graph-root")
|
||||
(assert/assert-is-visible
|
||||
"[role='application'][aria-label='Graph canvas']")
|
||||
(assert/assert-is-hidden ".graph-error")
|
||||
(page/goto-page "graph view beta")
|
||||
(assert/assert-graph-loaded?)))
|
||||
|
||||
(deftest graph-time-travel-playback-test
|
||||
(testing "time travel limits nodes and returning to Now restores the complete graph"
|
||||
(page/new-page "time travel old")
|
||||
(util/wait-timeout 20)
|
||||
(page/new-page "time travel new")
|
||||
(util/search-and-click "Go to graph view")
|
||||
(w/click "button[title*='Time'], button:has-text('Time travel')")
|
||||
(let [slider (w/-query "input[type='range']")]
|
||||
(assert/assert-is-visible slider)
|
||||
(let [full-count (util/count-elements ".graph-node, [data-node-id]")]
|
||||
(w/click slider)
|
||||
(k/press "Home")
|
||||
(is (<= (util/count-elements ".graph-node, [data-node-id]")
|
||||
full-count))
|
||||
(w/click ".graph-time-travel-reset[title='Now']")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".graph-time-travel-label" :has-text "Now"))
|
||||
(is (= full-count
|
||||
(util/count-elements ".graph-node, [data-node-id]")))))))
|
||||
|
||||
(deftest restoring-graph-gates-and-recovers-interaction-test
|
||||
(testing "reload/restoring never accepts half-mounted edits and restores all controls"
|
||||
(b/new-block "restore interaction target")
|
||||
(util/exit-edit)
|
||||
(w/refresh)
|
||||
(assert/assert-is-visible
|
||||
(loc/or ".loading-graph, .ui__loading"
|
||||
"[data-testid='page title']"))
|
||||
(when (w/visible? ".loading-graph, .ui__loading")
|
||||
(is (or (not (w/visible? ".block-add-button"))
|
||||
(.isDisabled (w/-query ".block-add-button")))))
|
||||
(assert/assert-graph-loaded?)
|
||||
(assert/assert-is-visible ".toolbar-dots-btn")
|
||||
(w/click ".toolbar-dots-btn")
|
||||
(assert/assert-is-visible "[role='menuitem']")
|
||||
(k/esc)
|
||||
(b/jump-to-block "restore interaction target")
|
||||
(util/move-cursor-to-end)
|
||||
(util/press-seq " after restore")
|
||||
(is (= "restore interaction target after restore" (util/get-edit-content)))))
|
||||
46
clj-e2e/test/logseq/e2e/graph_test.clj
Normal file
46
clj-e2e/test/logseq/e2e/graph_test.clj
Normal file
@@ -0,0 +1,46 @@
|
||||
(ns logseq.e2e.graph-test
|
||||
(:require [clojure.test :refer [deftest is]]
|
||||
[logseq.e2e.graph]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(deftest maybe-input-e2ee-password-skips-when-cloud-ready-test
|
||||
(let [wait-calls (atom 0)
|
||||
input-calls (atom 0)]
|
||||
(with-redefs-fn {#'w/visible? (fn [q] (= q "button.cloud.on.idle"))
|
||||
#'util/wait-timeout (fn [_] (swap! wait-calls inc))
|
||||
#'logseq.e2e.graph/input-e2ee-password (fn [] (swap! input-calls inc))}
|
||||
(fn []
|
||||
((var logseq.e2e.graph/maybe-input-e2ee-password))
|
||||
(is (= 8 @wait-calls))
|
||||
(is (zero? @input-calls))))))
|
||||
|
||||
(deftest maybe-input-e2ee-password-inputs-when-modal-appears-test
|
||||
(let [ticks (atom 0)
|
||||
input-calls (atom 0)]
|
||||
(with-redefs-fn {#'w/visible? (fn [q]
|
||||
(case q
|
||||
".e2ee-password-modal-content" (>= @ticks 2)
|
||||
"button.cloud.on.idle" false
|
||||
false))
|
||||
#'util/wait-timeout (fn [_] (swap! ticks inc))
|
||||
#'logseq.e2e.graph/input-e2ee-password (fn [] (swap! input-calls inc))}
|
||||
(fn []
|
||||
((var logseq.e2e.graph/maybe-input-e2ee-password))
|
||||
(is (= 2 @ticks))
|
||||
(is (= 1 @input-calls))))))
|
||||
|
||||
(deftest maybe-input-e2ee-password-does-not-exit-on-stale-cloud-ready-test
|
||||
(let [ticks (atom 0)
|
||||
input-calls (atom 0)]
|
||||
(with-redefs-fn {#'w/visible? (fn [q]
|
||||
(case q
|
||||
".e2ee-password-modal-content" (>= @ticks 2)
|
||||
"button.cloud.on.idle" true
|
||||
false))
|
||||
#'util/wait-timeout (fn [_] (swap! ticks inc))
|
||||
#'logseq.e2e.graph/input-e2ee-password (fn [] (swap! input-calls inc))}
|
||||
(fn []
|
||||
((var logseq.e2e.graph/maybe-input-e2ee-password))
|
||||
(is (= 2 @ticks))
|
||||
(is (= 1 @input-calls))))))
|
||||
49
clj-e2e/test/logseq/e2e/import_basic_test.clj
Normal file
49
clj-e2e/test/logseq/e2e/import_basic_test.clj
Normal file
@@ -0,0 +1,49 @@
|
||||
(ns logseq.e2e.import-basic-test
|
||||
(:require
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.graph :as graph]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w])
|
||||
(:import
|
||||
(java.nio.file Paths)))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each fixtures/new-logseq-page fixtures/validate-graph)
|
||||
|
||||
(defn- open-import!
|
||||
[]
|
||||
(util/double-esc)
|
||||
(w/click ".toolbar-dots-btn")
|
||||
(w/click (loc/filter "[role='menuitem']" :has-text "Import"))
|
||||
(w/wait-for ".importer"))
|
||||
|
||||
(deftest import-options-and-invalid-edn-preserve-current-graph-test
|
||||
(testing "browser import exposes supported sources and rejects invalid EDN atomically"
|
||||
(let [current-graph (ls-api-call! :app.getCurrentGraph)
|
||||
invalid-file (Paths/get "resources/invalid-db-export.edn"
|
||||
(into-array String []))
|
||||
graph-name (str "invalid-import-" (random-uuid))]
|
||||
(open-import!)
|
||||
(doseq [selector ["#import-sqlite-db"
|
||||
"#import-sqlite-zip"
|
||||
"#import-file-graph"
|
||||
"#import-debug-transit"
|
||||
"#import-db-edn"]]
|
||||
(assert/assert-have-count selector 1))
|
||||
(.setInputFiles (w/-query "#import-db-edn") invalid-file)
|
||||
(w/wait-for "#modal-headline")
|
||||
(w/click (loc/filter "button" :has-text "Submit"))
|
||||
(assert/assert-is-visible ".ui__toast")
|
||||
(assert/assert-is-visible "#modal-headline")
|
||||
(w/fill ".form-input" graph-name)
|
||||
(w/click (loc/filter "button" :has-text "Submit"))
|
||||
(w/wait-for ".ui__toast")
|
||||
(assert/assert-is-hidden ".ui__loading, .loading-graph")
|
||||
(is (= current-graph (ls-api-call! :app.getCurrentGraph)))
|
||||
(graph/goto-all-graphs)
|
||||
(assert/assert-is-hidden
|
||||
(loc/filter "#main-content-container" :has-text graph-name)))))
|
||||
57
clj-e2e/test/logseq/e2e/multi_tabs_basic_test.clj
Normal file
57
clj-e2e/test/logseq/e2e/multi_tabs_basic_test.clj
Normal file
@@ -0,0 +1,57 @@
|
||||
(ns logseq.e2e.multi-tabs-basic-test
|
||||
(:require [clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.graph :as graph]
|
||||
[logseq.e2e.playwright-page :as pw-page]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]
|
||||
[wally.repl :as repl]))
|
||||
|
||||
(use-fixtures :once fixtures/open-new-context)
|
||||
|
||||
(defn- add-blocks-and-check-on-other-tabs
|
||||
[new-blocks add-blocks-tab check-blocks-tabs]
|
||||
(w/with-page add-blocks-tab
|
||||
(b/new-blocks new-blocks))
|
||||
(run!
|
||||
#(w/with-page %
|
||||
(b/assert-blocks-visible new-blocks))
|
||||
check-blocks-tabs))
|
||||
|
||||
(deftest multi-tabs-test
|
||||
(testing "edit on one tab, check all tab's blocks are same"
|
||||
(pw-page/open-pages fixtures/*pw-ctx* 3)
|
||||
(let [[p1 p2 p3 :as pages] (pw-page/get-pages fixtures/*pw-ctx*)
|
||||
blocks-to-add (map #(str "b" %) (range 10))]
|
||||
(is (= 3 (count pages)))
|
||||
(add-blocks-and-check-on-other-tabs blocks-to-add p1 [p2 p3])))
|
||||
|
||||
(testing "add new graphs and switch graphs + edit + check on tabs"
|
||||
(let [[p1 p2 p3] (pw-page/get-pages fixtures/*pw-ctx*)]
|
||||
(letfn [(switch-to-graph-then-edit-and-check [graph-name]
|
||||
(w/with-page p2
|
||||
(util/goto-journals)
|
||||
(assert/assert-in-normal-mode?)
|
||||
(graph/switch-graph graph-name false false))
|
||||
(w/with-page p3
|
||||
(util/goto-journals)
|
||||
(assert/assert-in-normal-mode?)
|
||||
(graph/switch-graph graph-name false false))
|
||||
(w/with-page p1
|
||||
(util/goto-journals)
|
||||
(assert/assert-in-normal-mode?)
|
||||
(graph/switch-graph graph-name false false))
|
||||
(let [graph-new-blocks (map #(str graph-name "-b1-" %) (range 5))]
|
||||
(add-blocks-and-check-on-other-tabs graph-new-blocks p1 [p2 p3])))]
|
||||
(w/with-page p1
|
||||
(graph/new-graph "graph1" false)
|
||||
(graph/new-graph "graph2" false)
|
||||
(graph/new-graph "graph3" false))
|
||||
;; FIXME: since all-graphs isn't auto-update when other tabs add new graphs, so refresh here
|
||||
(w/with-page p2 (util/refresh-until-graph-loaded))
|
||||
(w/with-page p3 (util/refresh-until-graph-loaded))
|
||||
(switch-to-graph-then-edit-and-check "graph1")
|
||||
(switch-to-graph-then-edit-and-check "graph2")
|
||||
(switch-to-graph-then-edit-and-check "graph3")))))
|
||||
664
clj-e2e/test/logseq/e2e/outliner_basic_test.clj
Normal file
664
clj-e2e/test/logseq/e2e/outliner_basic_test.clj
Normal file
@@ -0,0 +1,664 @@
|
||||
(ns logseq.e2e.outliner-basic-test
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest testing is use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.graph :as graph]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.page :as p]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w])
|
||||
(:import
|
||||
(com.microsoft.playwright Locator$DragToOptions)))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(defn- block-text-position
|
||||
[text]
|
||||
(let [locator (w/find-one-by-text "span" text)]
|
||||
(assert/assert-is-visible locator)
|
||||
(first (util/bounding-xy locator))))
|
||||
|
||||
(defn create-test-page-and-insert-blocks []
|
||||
;; a page block and a child block
|
||||
(is (= 2 (util/blocks-count)))
|
||||
(b/new-blocks ["first block" "second block"])
|
||||
(util/exit-edit)
|
||||
(is (= 3 (util/blocks-count))))
|
||||
|
||||
(defn indent-and-outdent []
|
||||
(b/new-blocks ["b1" "b2"])
|
||||
(testing "simple indent and outdent"
|
||||
(b/indent)
|
||||
(b/outdent))
|
||||
|
||||
(testing "indent a block with its children"
|
||||
(b/new-block "b3")
|
||||
(b/indent)
|
||||
(k/arrow-up)
|
||||
(b/indent)
|
||||
(util/exit-edit)
|
||||
(let [[x1 x2 x3] (map block-text-position ["b1" "b2" "b3"])]
|
||||
(is (< x1 x2 x3))))
|
||||
|
||||
(testing "unindent a block with its children"
|
||||
(b/open-last-block)
|
||||
(b/new-blocks ["b4" "b5"])
|
||||
(b/indent)
|
||||
(k/arrow-up)
|
||||
(b/outdent)
|
||||
(util/exit-edit)
|
||||
(let [[x2 x3 x4 x5] (map block-text-position ["b2" "b3" "b4" "b5"])]
|
||||
(is (and (= x2 x4) (= x3 x5) (< x2 x3))))))
|
||||
|
||||
(defn indent-outdent-embed-page []
|
||||
(p/new-page "Page embed")
|
||||
(b/new-blocks ["b1" "b2"])
|
||||
(p/new-page "Page testing")
|
||||
(b/new-blocks ["b3" ""])
|
||||
(util/input-command "Node embed")
|
||||
(util/press-seq "Page embed" {:delay 60})
|
||||
(w/wait-for "#ac-0.menu-link:has-text('Page embed')")
|
||||
(k/press "Enter" {:delay 60})
|
||||
(util/exit-edit)
|
||||
(b/new-blocks ["b4"])
|
||||
(b/outdent)
|
||||
(b/indent)
|
||||
(util/exit-edit)
|
||||
(let [[x2 x3 x4] (map block-text-position ["b2" "b3" "b4"])]
|
||||
(is (= x2 x4))
|
||||
(is (< x3 x2))))
|
||||
|
||||
(defn move-up-down []
|
||||
(b/new-blocks ["b1" "b2" "b3" "b4"])
|
||||
(util/repeat-keyboard 2 "Shift+ArrowUp")
|
||||
(let [contents (util/get-page-blocks-contents)]
|
||||
(is (= contents ["b1" "b2" "b3" "b4"])))
|
||||
(util/repeat-keyboard 2 (str (if util/mac? "Meta" "Alt") "+Shift+ArrowUp"))
|
||||
(let [contents (util/get-page-blocks-contents)]
|
||||
(is (= contents ["b3" "b4" "b1" "b2"])))
|
||||
(util/repeat-keyboard 2 (str (if util/mac? "Meta" "Alt") "+Shift+ArrowDown"))
|
||||
(let [contents (util/get-page-blocks-contents)]
|
||||
(is (= contents ["b1" "b2" "b3" "b4"]))))
|
||||
|
||||
(defn- zoom-in-shortcut []
|
||||
(k/press (if util/mac? "Meta+Shift+." "Alt+ArrowRight")))
|
||||
|
||||
(defn- current-location-hash []
|
||||
(w/eval-js "window.location.hash"))
|
||||
|
||||
(defn- current-editing-block-id []
|
||||
(w/eval-js
|
||||
"(() => {
|
||||
const editor = document.querySelector('.editor-wrapper textarea');
|
||||
return editor?.closest('[blockid]')?.getAttribute('blockid') ?? null;
|
||||
})();"))
|
||||
|
||||
(deftest focused-root-block-cannot-indent-or-move-test
|
||||
(testing "Focused root block ignores indent/outdent/move-up/move-down commands"
|
||||
(b/new-blocks ["focused-root" "focused-child"])
|
||||
(k/arrow-up)
|
||||
(let [root-id (current-editing-block-id)]
|
||||
(is (string? root-id))
|
||||
(zoom-in-shortcut)
|
||||
(util/wait-timeout 400)
|
||||
;; Retry once in case the first key event gets swallowed by the editor.
|
||||
(when-not (string/includes? (or (current-location-hash) "") root-id)
|
||||
(zoom-in-shortcut)
|
||||
(util/wait-timeout 400))
|
||||
(is (string/includes? (or (current-location-hash) "") root-id))
|
||||
(util/wait-editor-visible)
|
||||
(is (= "focused-root" (util/get-edit-content)))
|
||||
(let [before-hash (current-location-hash)
|
||||
before-block-contents (util/get-page-blocks-contents)]
|
||||
(k/tab)
|
||||
(util/wait-timeout 100)
|
||||
(k/shift+tab)
|
||||
(util/wait-timeout 100)
|
||||
(k/meta+shift+arrow-up)
|
||||
(util/wait-timeout 100)
|
||||
(k/meta+shift+arrow-down)
|
||||
(util/wait-timeout 100)
|
||||
(is (= "focused-root" (util/get-edit-content)))
|
||||
(is (= before-hash (current-location-hash)))
|
||||
(is (= before-block-contents (util/get-page-blocks-contents)))))))
|
||||
|
||||
(defn delete []
|
||||
(testing "Delete blocks case 1"
|
||||
(b/new-blocks ["b1" "b2" "b3" "b4"])
|
||||
(b/delete-blocks) ; delete b4
|
||||
(util/repeat-keyboard 2 "Shift+ArrowUp") ; select b3 and b2
|
||||
(b/delete-blocks)
|
||||
(is (= "b1" (util/get-edit-content)))
|
||||
(is (= 1 (util/page-blocks-count)))))
|
||||
|
||||
(defn delete-end []
|
||||
(testing "Delete at end"
|
||||
(b/new-blocks ["b1" "b2" "b3"])
|
||||
(k/arrow-up)
|
||||
(k/delete)
|
||||
(is (= "b2b3" (util/get-edit-content)))
|
||||
(is (= 2 (util/page-blocks-count)))))
|
||||
|
||||
(defn delete-test-with-children []
|
||||
(testing "Delete block with its children"
|
||||
(b/new-blocks ["b1" "b2" "b3" "b4"])
|
||||
(b/indent)
|
||||
(k/arrow-up)
|
||||
(b/indent)
|
||||
(k/arrow-up)
|
||||
(b/delete-blocks)
|
||||
(util/wait-editor-visible)
|
||||
(is (= "b1" (util/get-edit-content)))
|
||||
(is (= 1 (util/page-blocks-count)))))
|
||||
|
||||
(deftest create-test-page-and-insert-blocks-test
|
||||
(create-test-page-and-insert-blocks))
|
||||
|
||||
(deftest indent-and-outdent-test
|
||||
(indent-and-outdent))
|
||||
|
||||
(deftest indent-outdent-embed-page-test
|
||||
(indent-outdent-embed-page))
|
||||
|
||||
(deftest move-up-down-test
|
||||
(move-up-down))
|
||||
|
||||
(deftest delete-test
|
||||
(delete))
|
||||
|
||||
(deftest delete-end-test
|
||||
(delete-end))
|
||||
|
||||
(deftest delete-test-with-children-test
|
||||
(delete-test-with-children))
|
||||
|
||||
(deftest delete-concat-test-2-blocks
|
||||
(testing "Delete concat with empty block"
|
||||
(b/new-blocks ["" "b2"])
|
||||
(b/indent)
|
||||
(k/arrow-up)
|
||||
(k/delete)
|
||||
(util/wait-editor-visible)
|
||||
(is (= "b2" (util/get-edit-content)))
|
||||
(util/exit-edit)
|
||||
(is (= ["b2"] (util/get-page-blocks-contents)))))
|
||||
|
||||
(deftest delete-concat-test-3-blocks
|
||||
(testing "Delete concat with empty block"
|
||||
(b/new-blocks ["" "b2" "b3"])
|
||||
(b/indent)
|
||||
(k/arrow-up)
|
||||
(k/arrow-up)
|
||||
(k/delete)
|
||||
(util/wait-editor-visible)
|
||||
(is (= "b2" (util/get-edit-content)))
|
||||
(util/exit-edit)
|
||||
(is (= ["b2" "b3"] (util/get-page-blocks-contents)))))
|
||||
|
||||
(deftest delete-concat-test-with-children
|
||||
(testing "Delete concat with children blocks"
|
||||
(b/new-blocks ["" "b2" "b3"])
|
||||
(b/indent)
|
||||
(k/arrow-up)
|
||||
(b/indent)
|
||||
(k/arrow-up)
|
||||
(k/delete)
|
||||
(util/wait-editor-visible)
|
||||
(is (= "" (util/get-edit-content)))
|
||||
(is (= 3 (util/page-blocks-count)))))
|
||||
|
||||
(deftest delete-concat-test-with-tag
|
||||
(testing "Delete concat with tag"
|
||||
(b/new-blocks ["" "b2"])
|
||||
(b/indent)
|
||||
(util/set-tag "tag1")
|
||||
(k/arrow-up)
|
||||
(k/delete)
|
||||
(util/wait-editor-visible)
|
||||
(is (= "b2" (util/get-edit-content)))
|
||||
(util/exit-edit)
|
||||
(assert/assert-is-visible
|
||||
".ls-block a.tag:has-text('tag1')")
|
||||
(is (= ["b2"] (util/get-page-blocks-contents)))))
|
||||
|
||||
(deftest backspace-empty-first-child-keeps-empty-parent-subtree-test
|
||||
(testing "Backspace in the first empty child of an empty parent deletes only the child"
|
||||
(p/new-page "backspace empty first child")
|
||||
(let [page-uuid (get (ls-api-call! :editor.getBlock "backspace empty first child") "uuid")
|
||||
[parent first-child child2 child3]
|
||||
(ls-api-call! :editor.insertBatchBlock
|
||||
page-uuid
|
||||
[{:content ""
|
||||
:children [{:content ""}
|
||||
{:content "child2"}
|
||||
{:content "child3"}]}])
|
||||
block-visible? #(pos? (util/count-elements (str "#ls-block-" %)))]
|
||||
(w/click (str "#ls-block-" (get first-child "uuid") " .block-content"))
|
||||
(util/wait-editor-visible)
|
||||
(is (= "" (util/get-edit-content)))
|
||||
(k/backspace)
|
||||
(util/wait-timeout 100)
|
||||
(is (not (block-visible? (get first-child "uuid"))))
|
||||
(is (block-visible? (get parent "uuid")))
|
||||
(is (block-visible? (get child2 "uuid")))
|
||||
(is (block-visible? (get child3 "uuid"))))))
|
||||
|
||||
(deftest backspace-at-parent-start-keeps-children-test
|
||||
(doseq [[page-name parent-content]
|
||||
[["backspace non-empty parent start" "a"]
|
||||
["backspace empty parent start" ""]]]
|
||||
(testing (str "Backspace at the start of parent " (pr-str parent-content)
|
||||
" preserves its child")
|
||||
(p/new-page page-name)
|
||||
(let [page-uuid (get (ls-api-call! :editor.getBlock page-name) "uuid")
|
||||
[parent child]
|
||||
(ls-api-call! :editor.insertBatchBlock
|
||||
page-uuid
|
||||
[{:content parent-content
|
||||
:children [{:content "b"}]}])
|
||||
parent-uuid (get parent "uuid")
|
||||
child-uuid (get child "uuid")
|
||||
block-visible? #(pos? (util/count-elements (str "#ls-block-" %)))]
|
||||
(w/click (str "#block-content-" parent-uuid))
|
||||
(util/wait-editor-visible)
|
||||
(w/eval-js
|
||||
"(() => {
|
||||
const editor = document.querySelector('.editor-wrapper textarea');
|
||||
editor.focus();
|
||||
editor.setSelectionRange(0, 0);
|
||||
})();")
|
||||
(k/backspace)
|
||||
(util/wait-timeout 200)
|
||||
(is (= parent-content (util/get-edit-content)))
|
||||
(is (block-visible? parent-uuid))
|
||||
(is (block-visible? child-uuid))))))
|
||||
|
||||
(deftest consecutive-backspace-does-not-restore-deleted-blocks-test
|
||||
(testing "Deleting b and then a never renders stale block content"
|
||||
(p/new-page "consecutive backspace deletion")
|
||||
(let [page-uuid (get (ls-api-call! :editor.getBlock "consecutive backspace deletion") "uuid")
|
||||
[_a b] (ls-api-call! :editor.insertBatchBlock
|
||||
page-uuid
|
||||
[{:content "a"}
|
||||
{:content "b"}])
|
||||
editor-state #(w/eval-js
|
||||
"(() => {
|
||||
const editor = document.querySelector('.editor-wrapper textarea');
|
||||
return [
|
||||
editor?.value ?? null,
|
||||
Array.from(document.querySelectorAll(
|
||||
'.ls-page-blocks .block-title-wrap'))
|
||||
.map((node) => node.textContent.trim())
|
||||
.filter(Boolean)
|
||||
];
|
||||
})();")]
|
||||
(w/click (str "#block-content-" (get b "uuid")))
|
||||
(util/wait-editor-visible)
|
||||
(w/eval-js
|
||||
"(() => {
|
||||
const editor = document.querySelector('.editor-wrapper textarea');
|
||||
editor.focus();
|
||||
editor.setSelectionRange(editor.value.length, editor.value.length);
|
||||
})();")
|
||||
(dotimes [_ 4]
|
||||
(k/backspace))
|
||||
(util/wait-timeout 50)
|
||||
(let [immediate (editor-state)]
|
||||
(is (= [] (second immediate)))
|
||||
(util/wait-timeout 500)
|
||||
(is (= immediate (editor-state)))))))
|
||||
|
||||
(deftest held-backspace-does-not-duplicate-merged-content-test
|
||||
(testing "Repeated Backspace cannot merge the same block twice"
|
||||
(p/new-page "held backspace merge")
|
||||
(b/new-blocks ["Foo" "" "Foo" "" "Foo" ""])
|
||||
(util/repeat-keyboard 75 "Backspace")
|
||||
(util/wait-timeout 500)
|
||||
(let [editor-content (util/get-edit-content)
|
||||
block-contents (util/get-page-blocks-contents)]
|
||||
(is (not= "FooFoo" editor-content))
|
||||
(is (not-any? #{"FooFoo"} block-contents)))))
|
||||
|
||||
(deftest rapid-retype-before-enter-keeps-the-edit-test
|
||||
(testing "Backspace, retype, and Enter keep the retyped content"
|
||||
(b/open-last-block)
|
||||
(k/press "a")
|
||||
(k/press "Backspace")
|
||||
(k/press "a")
|
||||
(k/press "Enter")
|
||||
(util/wait-timeout 700)
|
||||
(util/exit-edit)
|
||||
(is (= ["a"] (remove string/blank?
|
||||
(util/get-page-blocks-contents))))))
|
||||
|
||||
(defn- block-tree
|
||||
[page-name]
|
||||
(ls-api-call! :editor.getPageBlocksTree page-name))
|
||||
|
||||
(defn- content-tree
|
||||
[nodes]
|
||||
(mapv (fn [node]
|
||||
[(get node "content")
|
||||
(content-tree (get node "children"))])
|
||||
nodes))
|
||||
|
||||
(defn- wait-for-block-content!
|
||||
[block-id expected]
|
||||
(loop [attempts-left 40]
|
||||
(let [actual (get (ls-api-call! :editor.getBlock block-id) "content")]
|
||||
(if (or (= expected actual) (zero? attempts-left))
|
||||
(is (= expected actual))
|
||||
(do
|
||||
(util/wait-timeout 100)
|
||||
(recur (dec attempts-left)))))))
|
||||
|
||||
(defn- visible-outline-content-tree
|
||||
[]
|
||||
(let [relations
|
||||
(w/eval-js
|
||||
"(() => {
|
||||
const nodes = [...document.querySelectorAll('.ls-page-blocks .ls-block')]
|
||||
.filter(node => !node.closest('.is-comments-area, .ls-comments-area, .ls-comment-row'));
|
||||
return nodes
|
||||
.map((node, index) => {
|
||||
const own = selector => [...node.querySelectorAll(selector)]
|
||||
.find(child => child.closest('.ls-block') === node);
|
||||
const editor = own('.editor-wrapper textarea');
|
||||
const content = own('.block-content');
|
||||
const title = (editor && editor.offsetParent !== null
|
||||
? editor.value
|
||||
: content?.innerText || node.dataset.blockTitle || '').trim();
|
||||
const parent = node.parentElement.closest('.ls-block');
|
||||
return [index, title, parent ? nodes.indexOf(parent) : null];
|
||||
})
|
||||
.filter(([_index, title]) => title);
|
||||
})()")
|
||||
children-by-parent (group-by #(nth % 2) relations)
|
||||
build-tree (fn build-tree [parent]
|
||||
(mapv (fn [[index title]]
|
||||
[title (build-tree index)])
|
||||
(get children-by-parent parent)))]
|
||||
(build-tree nil)))
|
||||
|
||||
(defn- wait-for-content-tree!
|
||||
[_page-name expected]
|
||||
(util/wait-timeout 300)
|
||||
(loop [attempts 40]
|
||||
(let [actual (visible-outline-content-tree)]
|
||||
(if (= expected actual)
|
||||
(do
|
||||
(util/wait-timeout 250)
|
||||
(is (= expected (visible-outline-content-tree))))
|
||||
(if (zero? attempts)
|
||||
(is (= expected actual))
|
||||
(do
|
||||
(util/wait-timeout 100)
|
||||
(recur (dec attempts))))))))
|
||||
|
||||
(defn- undo-and-wait-for-content-tree!
|
||||
[page-name expected]
|
||||
(b/undo)
|
||||
(util/wait-timeout 1000)
|
||||
(wait-for-content-tree! page-name expected))
|
||||
|
||||
(defn- redo-and-wait-for-content-tree!
|
||||
[page-name expected]
|
||||
(b/redo)
|
||||
(util/wait-timeout 1000)
|
||||
(wait-for-content-tree! page-name expected))
|
||||
|
||||
(defn- click-block-by-uuid!
|
||||
[uuid]
|
||||
(w/click (.first (w/-query (str "#block-content-" uuid ":visible")))))
|
||||
|
||||
(defn- click-block-by-title!
|
||||
[title]
|
||||
(let [target-selector
|
||||
(w/eval-js
|
||||
(format
|
||||
"(() => {
|
||||
const root = document.querySelector(\".ls-block[data-block-title='%s']\");
|
||||
const own = selector => [...root.querySelectorAll(selector)]
|
||||
.find(node => node.closest('.ls-block') === root);
|
||||
const editor = own('textarea');
|
||||
const target = editor && editor.offsetParent !== null
|
||||
? editor
|
||||
: own('.block-content');
|
||||
return `#${CSS.escape(target.id)}`;
|
||||
})()"
|
||||
title))]
|
||||
(w/click target-selector)
|
||||
(w/eval-js
|
||||
(format
|
||||
"(() => new Promise((resolve, reject) => {
|
||||
let attempts = 40;
|
||||
const waitForTarget = () => {
|
||||
const root = document.querySelector(\".ls-block[data-block-title='%s']\");
|
||||
const target = [...root.querySelectorAll('textarea')]
|
||||
.find(node => node.closest('.ls-block') === root && node.offsetParent !== null);
|
||||
if (target) {
|
||||
target.focus();
|
||||
resolve(true);
|
||||
} else if (attempts-- === 0) {
|
||||
reject(new Error('Target block editor did not become active'));
|
||||
} else {
|
||||
setTimeout(waitForTarget, 50);
|
||||
}
|
||||
};
|
||||
waitForTarget();
|
||||
}))()"
|
||||
title))))
|
||||
|
||||
(defn- move-editor-cursor-to-start!
|
||||
[]
|
||||
(w/eval-js
|
||||
"(() => {
|
||||
const editor = document.activeElement?.matches('.editor-wrapper textarea')
|
||||
? document.activeElement
|
||||
: [...document.querySelectorAll('.editor-wrapper textarea')]
|
||||
.find(node => node.offsetParent !== null);
|
||||
editor.focus();
|
||||
editor.setSelectionRange(0, 0);
|
||||
})();"))
|
||||
|
||||
(defn- move-editor-cursor-to!
|
||||
[position]
|
||||
(w/eval-js
|
||||
(format
|
||||
"(() => {
|
||||
const editor = document.activeElement?.matches('.editor-wrapper textarea')
|
||||
? document.activeElement
|
||||
: [...document.querySelectorAll('.editor-wrapper textarea')]
|
||||
.find(node => node.offsetParent !== null);
|
||||
editor.focus();
|
||||
editor.setSelectionRange(%d, %d);
|
||||
})();"
|
||||
position
|
||||
position)))
|
||||
|
||||
(defn- move-editor-cursor-to-end!
|
||||
[]
|
||||
(w/eval-js
|
||||
"(() => {
|
||||
const editor = document.activeElement?.matches('.editor-wrapper textarea')
|
||||
? document.activeElement
|
||||
: [...document.querySelectorAll('.editor-wrapper textarea')]
|
||||
.find(node => node.offsetParent !== null);
|
||||
editor.focus();
|
||||
editor.setSelectionRange(editor.value.length, editor.value.length);
|
||||
})();"))
|
||||
|
||||
(deftest boundary-delete-and-backspace-merge-contract-test
|
||||
(testing "boundary edits preserve structure through per-step and full-history undo/redo"
|
||||
(let [page-name "boundary edit history"
|
||||
page-uuid (do
|
||||
(p/new-page page-name)
|
||||
(get (ls-api-call! :editor.getBlock page-name) "uuid"))
|
||||
[_left right _child _source _source-child leaf]
|
||||
(ls-api-call! :editor.insertBatchBlock
|
||||
page-uuid
|
||||
[{:content "b"}
|
||||
{:content "c"
|
||||
:children [{:content "d"}]}
|
||||
{:content "e"
|
||||
:children [{:content "f"}]}
|
||||
{:content "g"}])
|
||||
initial [["b" []]
|
||||
["c" [["d" []]]]
|
||||
["e" [["f" []]]]
|
||||
["g" []]]
|
||||
after-backspace [["bc" [["d" []]]]
|
||||
["e" [["f" []]]]
|
||||
["g" []]]
|
||||
after-enter [["b" [["c" []]
|
||||
["d" []]]]
|
||||
["e" [["f" []]]]
|
||||
["g" []]]
|
||||
after-delete [["b" [["c" []]
|
||||
["d" []]]]
|
||||
["e" [["fg" []]]]]
|
||||
assert-tree! (partial wait-for-content-tree! page-name)]
|
||||
(click-block-by-uuid! (get right "uuid"))
|
||||
(util/wait-editor-visible)
|
||||
(move-editor-cursor-to-start!)
|
||||
(k/backspace)
|
||||
(assert-tree! after-backspace)
|
||||
(is (= "bc" (util/get-edit-content)))
|
||||
(is (= 1
|
||||
(w/eval-js
|
||||
"document.querySelector('.editor-wrapper textarea').selectionStart")))
|
||||
(undo-and-wait-for-content-tree! page-name initial)
|
||||
(redo-and-wait-for-content-tree! page-name after-backspace)
|
||||
|
||||
(click-block-by-title! "bc")
|
||||
(util/wait-editor-visible)
|
||||
(move-editor-cursor-to! 1)
|
||||
(is (= ["bc" 1]
|
||||
(w/eval-js
|
||||
"(() => {
|
||||
const editor = document.activeElement;
|
||||
return [editor.value, editor.selectionStart];
|
||||
})()")))
|
||||
(k/enter)
|
||||
(util/wait-timeout 1000)
|
||||
(assert-tree! after-enter)
|
||||
(undo-and-wait-for-content-tree! page-name after-backspace)
|
||||
(redo-and-wait-for-content-tree! page-name after-enter)
|
||||
|
||||
(click-block-by-title! "e")
|
||||
(util/wait-editor-visible)
|
||||
(move-editor-cursor-to-start!)
|
||||
(k/backspace)
|
||||
(assert-tree! after-enter)
|
||||
|
||||
(click-block-by-title! "d")
|
||||
(util/wait-editor-visible)
|
||||
(move-editor-cursor-to-end!)
|
||||
(k/delete)
|
||||
(assert-tree! after-enter)
|
||||
|
||||
(click-block-by-title! "f")
|
||||
(util/wait-editor-visible)
|
||||
(move-editor-cursor-to-end!)
|
||||
(k/delete)
|
||||
(assert-tree! after-delete)
|
||||
(is (nil? (ls-api-call! :editor.getBlock (get leaf "uuid"))))
|
||||
(undo-and-wait-for-content-tree! page-name after-enter)
|
||||
(redo-and-wait-for-content-tree! page-name after-delete)
|
||||
|
||||
(undo-and-wait-for-content-tree! page-name after-enter)
|
||||
(undo-and-wait-for-content-tree! page-name after-backspace)
|
||||
(undo-and-wait-for-content-tree! page-name initial))))
|
||||
|
||||
(defn- drag-block!
|
||||
[source-title target-title placement]
|
||||
(let [source-block (w/-query
|
||||
(format ".ls-page-blocks .ls-block[data-block-title='%s']"
|
||||
source-title))
|
||||
target-block (w/-query
|
||||
(format ".ls-page-blocks .ls-block[data-block-title='%s']"
|
||||
target-title))
|
||||
target-height (.-height (.boundingBox target-block))
|
||||
target-x (if (= placement "inside") 80 30)
|
||||
target-y (case placement
|
||||
"before" 2
|
||||
"after" (- target-height 2)
|
||||
"inside" (- target-height 2))
|
||||
options (doto (Locator$DragToOptions.)
|
||||
(.setTargetPosition target-x target-y)
|
||||
(.setSteps 12))]
|
||||
(.dragTo (.first (.locator source-block ".bullet-container"))
|
||||
target-block
|
||||
options)
|
||||
(util/wait-timeout 250)))
|
||||
|
||||
(deftest drag-reorders-once-and-is-undoable-test
|
||||
(testing "same-level drag changes order once and undo restores the original order"
|
||||
(let [page-name (p/get-page-name)
|
||||
initial [["drag first" []]
|
||||
["drag second" []]
|
||||
["drag third" []]]
|
||||
reordered [["drag third" []]
|
||||
["drag first" []]
|
||||
["drag second" []]]]
|
||||
(b/new-blocks ["drag first" "drag second" "drag third"])
|
||||
(util/exit-edit)
|
||||
(wait-for-content-tree! page-name initial)
|
||||
(drag-block! "drag third" "drag first" "before")
|
||||
(wait-for-content-tree! page-name reordered)
|
||||
(undo-and-wait-for-content-tree! page-name initial))))
|
||||
|
||||
(deftest drag-indents-and-outdents-test
|
||||
(testing "dragging into and out of a parent updates both sides of the tree"
|
||||
(let [page-name (p/get-page-name)
|
||||
initial [["drag parent" []]
|
||||
["drag child candidate" []]
|
||||
["drag tail" []]]
|
||||
indented [["drag parent" [["drag child candidate" []]]]
|
||||
["drag tail" []]]
|
||||
outdented [["drag parent" []]
|
||||
["drag tail" []]
|
||||
["drag child candidate" []]]]
|
||||
(b/new-blocks ["drag parent" "drag child candidate" "drag tail"])
|
||||
(util/exit-edit)
|
||||
(wait-for-content-tree! page-name initial)
|
||||
(drag-block! "drag child candidate" "drag parent" "inside")
|
||||
(wait-for-content-tree! page-name indented)
|
||||
(drag-block! "drag child candidate" "drag tail" "after")
|
||||
(wait-for-content-tree! page-name outdented))))
|
||||
|
||||
(deftest drag-rejects-parent-into-descendant-test
|
||||
(testing "dragging a parent into its descendant leaves the tree unchanged"
|
||||
(let [page-name (p/get-page-name)]
|
||||
(b/new-blocks ["cycle parent" "cycle child"])
|
||||
(b/indent)
|
||||
(util/exit-edit)
|
||||
(let [before (content-tree (block-tree page-name))]
|
||||
(drag-block! "cycle parent" "cycle child" "inside")
|
||||
(is (= before (content-tree (block-tree page-name))))))))
|
||||
|
||||
(deftest undo-history-is-scoped-to-current-graph-test
|
||||
(testing "Undo in a newly switched graph cannot consume the previous graph history"
|
||||
(let [graph-a (str "undo-a-" (random-uuid))
|
||||
graph-b (str "undo-b-" (random-uuid))]
|
||||
(graph/new-graph graph-a false)
|
||||
(p/new-page "undo graph a page")
|
||||
(b/new-block "graph a history")
|
||||
(let [graph-a-block-id (current-editing-block-id)]
|
||||
(wait-for-block-content! graph-a-block-id "graph a history")
|
||||
(graph/new-graph graph-b false)
|
||||
(p/new-page "undo graph b page")
|
||||
(b/new-block "graph b history")
|
||||
(let [graph-b-block-id (current-editing-block-id)]
|
||||
(wait-for-block-content! graph-b-block-id "graph b history")
|
||||
(b/undo)
|
||||
(wait-for-block-content! graph-b-block-id "")
|
||||
(graph/switch-graph graph-a false false)
|
||||
(wait-for-block-content! graph-a-block-id "graph a history"))))))
|
||||
399
clj-e2e/test/logseq/e2e/plugins_basic_test.clj
Normal file
399
clj-e2e/test/logseq/e2e/plugins_basic_test.clj
Normal file
@@ -0,0 +1,399 @@
|
||||
(ns logseq.e2e.plugins-basic-test
|
||||
(:require
|
||||
[clojure.set :as set]
|
||||
[clojure.test :refer [deftest testing is use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]
|
||||
[wally.repl :as repl]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each fixtures/new-logseq-page)
|
||||
|
||||
(defn ->plugin-ident
|
||||
[property-name]
|
||||
(str ":plugin.property._test_plugin/" property-name))
|
||||
|
||||
(defonce ^:private *property-idx (atom 0))
|
||||
(defn- new-property
|
||||
[]
|
||||
(str "test-property-" (swap! *property-idx inc)))
|
||||
|
||||
(defn- assert-api-ls-block!
|
||||
([ret] (assert-api-ls-block! ret 1))
|
||||
([ret-or-uuid count]
|
||||
(let [uuid' (or (get ret-or-uuid "uuid") ret-or-uuid)]
|
||||
(is (string? uuid'))
|
||||
(assert/assert-have-count (str "#ls-block-" uuid') count)
|
||||
uuid')))
|
||||
|
||||
(deftest editor-apis-test
|
||||
(testing "editor related apis"
|
||||
(page/new-page "test-block-apis")
|
||||
(ls-api-call! :ui.showMsg "hello world" "info")
|
||||
(let [ret (ls-api-call! :editor.appendBlockInPage "test-block-apis" "append-block-in-page-0")
|
||||
ret1 (ls-api-call! :editor.appendBlockInPage "append-block-in-current-page-0")
|
||||
uuid' (assert-api-ls-block! ret)]
|
||||
(assert-api-ls-block! ret1)
|
||||
(-> (ls-api-call! :editor.insertBlock uuid' "insert-0")
|
||||
(assert-api-ls-block!))
|
||||
(ls-api-call! :editor.updateBlock uuid' "append-but-updated-0")
|
||||
(k/esc)
|
||||
(w/wait-for ".block-title-wrap:text('append-but-updated-0')")
|
||||
(ls-api-call! :editor.removeBlock uuid')
|
||||
(assert-api-ls-block! uuid' 0))))
|
||||
|
||||
(deftest append-block-in-page-stays-at-page-root-test
|
||||
(page/new-page "test-append-block-root")
|
||||
(let [page (ls-api-call! :editor.getPage "test-append-block-root")
|
||||
root (ls-api-call! :editor.appendBlockInPage "test-append-block-root" "root")
|
||||
nested (ls-api-call! :editor.insertBlock (get root "uuid") "nested-1" {:sibling false})
|
||||
_ (ls-api-call! :editor.insertBlock (get nested "uuid") "nested-2" {:sibling true})
|
||||
appended (ls-api-call! :editor.appendBlockInPage "test-append-block-root" "appended")
|
||||
appended-block (ls-api-call! :editor.getBlock (get appended "uuid"))]
|
||||
(is (= (get page "id")
|
||||
(get-in appended-block ["parent" "id"])))))
|
||||
|
||||
(deftest block-properties-test
|
||||
(testing "block properties related apis"
|
||||
(page/new-page "test-block-properties-apis")
|
||||
(let [ret (ls-api-call! :editor.appendBlockInPage "test-block-properties-apis" "block-in-page-0" {:properties {:new-p1 1}})
|
||||
uuid' (assert-api-ls-block! ret)
|
||||
prop1 (ls-api-call! :editor.getBlockProperty uuid' "new-p1")
|
||||
props1 (ls-api-call! :editor.getBlockProperties uuid' "new-p1")
|
||||
props2 (ls-api-call! :editor.getPageProperties "test-block-properties-apis")]
|
||||
(w/wait-for ".property-k:text('new-p1')")
|
||||
(is (= 1 (get prop1 "value")))
|
||||
(is (= (get prop1 "ident") ":plugin.property._test_plugin/new-p1"))
|
||||
(is (= 1 (get props1 ":plugin.property._test_plugin/new-p1")))
|
||||
(is (= ["Page"] (get props2 ":block/tags")))
|
||||
(ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2")
|
||||
(ls-api-call! :editor.upsertBlockProperty uuid' "p3" true)
|
||||
(ls-api-call! :editor.upsertBlockProperty uuid' "p4" {:a 1, :b [2, 3]})
|
||||
(let [prop2 (ls-api-call! :editor.getBlockProperty uuid' "p2")
|
||||
prop3 (ls-api-call! :editor.getBlockProperty uuid' "p3")
|
||||
prop4 (ls-api-call! :editor.getBlockProperty uuid' "p4")]
|
||||
(w/wait-for ".property-k:text('p2')")
|
||||
(is (= "p2" (get prop2 "value")))
|
||||
(is (true? prop3))
|
||||
(is (= prop4 {"a" 1, "b" [2 3]})))
|
||||
(ls-api-call! :editor.removeBlockProperty uuid' "p4")
|
||||
;; wait for react re-render
|
||||
(util/wait-timeout 16)
|
||||
(is (nil? (w/find-one-by-text ".property-k" "p4")))
|
||||
(ls-api-call! :editor.upsertBlockProperty uuid' "p3" false)
|
||||
(ls-api-call! :editor.upsertBlockProperty uuid' "p2" "p2-updated")
|
||||
(w/wait-for ".block-title-wrap:text('p2-updated')")
|
||||
(let [props (ls-api-call! :editor.getBlockProperties uuid')]
|
||||
(is (= (get props ":plugin.property._test_plugin/p3") false))
|
||||
(is (= (get props ":plugin.property._test_plugin/p2") "p2-updated"))))))
|
||||
|
||||
(deftest property-upsert-test
|
||||
(testing "property with default settings"
|
||||
(let [p (new-property)]
|
||||
(ls-api-call! :editor.upsertProperty p)
|
||||
(let [property (ls-api-call! :editor.getProperty p)]
|
||||
(is (= "default" (get property "type")))
|
||||
(is (= ":db.cardinality/one" (get property "cardinality"))))))
|
||||
(testing "property with specified cardinality && type"
|
||||
(let [p (new-property)]
|
||||
(ls-api-call! :editor.upsertProperty p {:type "number"
|
||||
:cardinality "one"})
|
||||
(let [property (ls-api-call! :editor.getProperty p)]
|
||||
(is (= "number" (get property "type")))
|
||||
(is (= ":db.cardinality/one" (get property "cardinality")))))
|
||||
(let [p (new-property)]
|
||||
(ls-api-call! :editor.upsertProperty p {:type "number"
|
||||
:cardinality "many"})
|
||||
(let [property (ls-api-call! :editor.getProperty p)]
|
||||
(is (= "number" (get property "type")))
|
||||
(is (= ":db.cardinality/many" (get property "cardinality"))))
|
||||
(ls-api-call! :editor.upsertProperty p {:type "default"})
|
||||
(let [property (ls-api-call! :editor.getProperty p)]
|
||||
(is (= "default" (get property "type"))))))
|
||||
;; TODO: How to test against eval-js errors on playwright?
|
||||
#_(testing ":checkbox property doesn't allow :many cardinality"
|
||||
(let [p (new-property)]
|
||||
(ls-api-call! :editor.upsertProperty p {:type "checkbox"
|
||||
:cardinality "many"}))))
|
||||
|
||||
(deftest property-related-test
|
||||
(testing "properties management related apis"
|
||||
(doseq [property-type ["default" "number" "date" "datetime" "checkbox" "url" "node" "json" "string"]]
|
||||
(let [property-name (new-property)
|
||||
_ (ls-api-call! :editor.upsertProperty property-name {:type property-type})
|
||||
property (ls-api-call! :editor.getProperty property-name)]
|
||||
(is (= (get property "ident") (str ":plugin.property._test_plugin/" property-name)))
|
||||
(is (= (get property "type") property-type))
|
||||
(ls-api-call! :editor.removeProperty property-name)
|
||||
(is (nil? (ls-api-call! :editor.getProperty property-name)))))))
|
||||
|
||||
(deftest insert-block-with-properties
|
||||
(testing "insert block with properties"
|
||||
(let [page "insert-block-properties-test"
|
||||
_ (page/new-page page)
|
||||
;; :checkbox, :number, :url, :json can be inferred and default to :default, but not for :page
|
||||
b1 (ls-api-call! :editor.insertBlock page "b1" {:properties {"x1" true
|
||||
"x2" "https://logseq.com"
|
||||
"x3" 1
|
||||
"x4" [1]
|
||||
"x5" {:foo "bar"}
|
||||
"x6" "Page x"
|
||||
"x7" ["Page y" "Page z"]
|
||||
"x8" "some content"}
|
||||
:schema {"x6" {:type "page"}
|
||||
"x7" {:type "page"}}})]
|
||||
(is (true? (get b1 (->plugin-ident "x1"))))
|
||||
(is (= "https://logseq.com" (-> (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x2")))
|
||||
(get "title"))))
|
||||
(is (= 1 (-> (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x3")))
|
||||
(get ":logseq.property/value"))))
|
||||
(is (= 1 (-> (ls-api-call! :editor.getBlock (first (get b1 (->plugin-ident "x4"))))
|
||||
(get ":logseq.property/value"))))
|
||||
(is (= "{\"foo\":\"bar\"}" (get b1 (->plugin-ident "x5"))))
|
||||
(let [page-x (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x6")))]
|
||||
(is (= "page x" (get page-x "name"))))
|
||||
(is (= ["page y" "page z"] (map #(-> (ls-api-call! :editor.getBlock %)
|
||||
(get "name")) (get b1 (->plugin-ident "x7")))))
|
||||
(let [x8-block-value (ls-api-call! :editor.getBlock (get b1 (->plugin-ident "x8")))]
|
||||
(is (= "some content" (get x8-block-value "title")))
|
||||
(is (some? (get x8-block-value "page")))))))
|
||||
|
||||
(deftest update-block-with-properties
|
||||
(testing "update block with properties"
|
||||
(let [page "update-block-properties-test"
|
||||
_ (page/new-page page)
|
||||
block (ls-api-call! :editor.insertBlock page "b1")
|
||||
_ (ls-api-call! :editor.updateBlock (get block "uuid")
|
||||
"b1-new-content"
|
||||
{:properties {"y1" true
|
||||
"y2" "https://logseq.com"
|
||||
"y3" 1
|
||||
"y4" [1]
|
||||
"y5" {:foo "bar"}
|
||||
"y6" "Page x"
|
||||
"y7" ["Page y" "Page z"]
|
||||
"y8" "some content"}
|
||||
:schema {"y6" {:type "page"}
|
||||
"y7" {:type "page"}}})
|
||||
b1 (ls-api-call! :editor.getBlock (get block "uuid"))]
|
||||
(is (true? (get b1 (->plugin-ident "y1"))))
|
||||
(is (= "https://logseq.com" (-> (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y2") "id"]))
|
||||
(get "title"))))
|
||||
(is (= 1 (-> (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y3") "id"]))
|
||||
(get ":logseq.property/value"))))
|
||||
(is (= 1 (-> (ls-api-call! :editor.getBlock (get (first (get b1 (->plugin-ident "y4"))) "id"))
|
||||
(get ":logseq.property/value"))))
|
||||
(is (= "{\"foo\":\"bar\"}" (get b1 (->plugin-ident "y5"))))
|
||||
(let [page-x (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y6") "id"]))]
|
||||
(is (= "page x" (get page-x "name"))))
|
||||
(is (= ["page y" "page z"] (map #(-> (ls-api-call! :editor.getBlock %)
|
||||
(get "name"))
|
||||
(map #(get % "id") (get b1 (->plugin-ident "y7"))))))
|
||||
(let [y8-block-value (ls-api-call! :editor.getBlock (get-in b1 [(->plugin-ident "y8") "id"]))]
|
||||
(is (= "some content" (get y8-block-value "title")))
|
||||
(is (some? (get y8-block-value "page")))))))
|
||||
|
||||
(deftest insert-batch-blocks-test
|
||||
(testing "insert batch blocks"
|
||||
(let [page "insert batch blocks"
|
||||
_ (page/new-page page)
|
||||
page-uuid (get (ls-api-call! :editor.getBlock page) "uuid")
|
||||
result (ls-api-call! :editor.insertBatchBlock page-uuid
|
||||
[{:content "b1"
|
||||
:children [{:content "b1.1"
|
||||
:children [{:content "b1.1.1"}
|
||||
{:content "b1.1.2"}]}
|
||||
{:content "b1.2"}]}
|
||||
{:content "b2"}])
|
||||
contents (util/get-page-blocks-contents)]
|
||||
(is (= contents ["b1" "b1.1" "b1.1.1" "b1.1.2" "b1.2" "b2"]))
|
||||
(is (= (map #(get % "title") result) ["b1" "b1.1" "b1.1.1" "b1.1.2" "b1.2" "b2"]))))
|
||||
(testing "insert batch blocks with properties"
|
||||
(let [page "insert batch blocks with properties"
|
||||
_ (page/new-page page)
|
||||
page-uuid (get (ls-api-call! :editor.getBlock page) "uuid")
|
||||
result (ls-api-call! :editor.insertBatchBlock page-uuid
|
||||
[{:content "b1"
|
||||
:children [{:content "b1.1"
|
||||
:children [{:content "b1.1.1"
|
||||
:properties {"z3" "Page 1"
|
||||
"z4" ["Page 2" "Page 3"]}}
|
||||
{:content "b1.1.2"}]}
|
||||
{:content "b1.2"}]
|
||||
:properties {"z1" "test"
|
||||
"z2" true}}
|
||||
{:content "b2"}]
|
||||
{:schema {"z3" "page"
|
||||
"z4" "page"}})
|
||||
_ (assert/assert-is-visible ".block-title-wrap:text('Page 3')")
|
||||
contents (util/get-page-blocks-contents)]
|
||||
(is (= contents
|
||||
["b1" "test" "b1.1" "b1.1.1" "Page 1" "Page 2" "Page 3" "b1.1.2" "b1.2" "b2"]))
|
||||
(is (true? (get (first result) (->plugin-ident "z2")))))))
|
||||
|
||||
(deftest create-page-test
|
||||
(testing "create page"
|
||||
(let [result (ls-api-call! :editor.createPage "Test page 1")]
|
||||
(is (= "Test page 1" (get result "title")))
|
||||
(is
|
||||
(=
|
||||
":logseq.class/Page"
|
||||
(-> (ls-api-call! :editor.getBlock (first (get result "tags")))
|
||||
(get "ident"))))))
|
||||
|
||||
(testing "create page with properties"
|
||||
(let [result (ls-api-call! :editor.createPage "Test page 2"
|
||||
{:px1 "test"
|
||||
:px2 1
|
||||
:px3 "Page 1"
|
||||
:px4 ["Page 2" "Page 3"]}
|
||||
{:schema {:px3 {:type "page"}
|
||||
:px4 {:type "page"}}})
|
||||
page (ls-api-call! :editor.getBlock "Test page 2")]
|
||||
(is (= "Test page 2" (get result "title")))
|
||||
(is
|
||||
(=
|
||||
":logseq.class/Page"
|
||||
(-> (ls-api-call! :editor.getBlock (first (get result "tags")))
|
||||
(get "ident"))))
|
||||
;; verify properties
|
||||
(is (= "test" (-> (ls-api-call! :editor.getBlock (get-in page [(->plugin-ident "px1") "id"]))
|
||||
(get "title"))))
|
||||
(is (= 1 (-> (ls-api-call! :editor.getBlock (get-in page [(->plugin-ident "px2") "id"]))
|
||||
(get ":logseq.property/value"))))
|
||||
(let [page-1 (ls-api-call! :editor.getBlock (get-in page [(->plugin-ident "px3") "id"]))]
|
||||
(is (= "page 1" (get page-1 "name"))))
|
||||
(is (= ["page 2" "page 3"] (map #(-> (ls-api-call! :editor.getBlock %)
|
||||
(get "name"))
|
||||
(map #(get % "id") (get page (->plugin-ident "px4"))))))))
|
||||
|
||||
(testing "create tag page"
|
||||
(let [result (ls-api-call! :editor.createPage "Tag new"
|
||||
{}
|
||||
{:class true})]
|
||||
(is
|
||||
(=
|
||||
":logseq.class/Tag"
|
||||
(-> (ls-api-call! :editor.getBlock (first (get result "tags")))
|
||||
(get "ident")))))))
|
||||
|
||||
(deftest get-all-tags-test
|
||||
(testing "get_all_tags"
|
||||
(let [result (ls-api-call! :editor.get_all_tags)
|
||||
built-in-tags #{":logseq.class/Template"
|
||||
":logseq.class/Query"
|
||||
":logseq.class/Math-block"
|
||||
":logseq.class/Task"
|
||||
":logseq.class/Code-block"
|
||||
":logseq.class/Card"
|
||||
":logseq.class/Quote-block"
|
||||
":logseq.class/Cards"}]
|
||||
(is (set/subset? built-in-tags (set (map #(get % "ident") result)))))))
|
||||
|
||||
(deftest get-all-properties-test
|
||||
(testing "get_all_properties"
|
||||
(let [result (ls-api-call! :editor.get_all_properties)]
|
||||
(is (>= (count result) 20)))))
|
||||
|
||||
(deftest get-tag-objects-test
|
||||
(testing "get_tag_objects"
|
||||
(let [page "tag objects test"
|
||||
_ (page/new-page page)
|
||||
_ (ls-api-call! :editor.insertBlock page "task 1"
|
||||
{:properties {"logseq.property/status" "Doing"}})
|
||||
result (ls-api-call! :editor.get_tag_objects "logseq.class/Task")]
|
||||
(is (= (count result) 1))
|
||||
(is (= "task 1" (get (first result) "title"))))))
|
||||
|
||||
(deftest create-and-get-tag-test
|
||||
(testing "create and get tag with title or ident"
|
||||
(let [title "book1"
|
||||
title-ident (str :plugin.class._test_plugin/book1)
|
||||
tag1 (ls-api-call! :editor.createTag title)
|
||||
tag2 (ls-api-call! :editor.getTag title)
|
||||
tag3 (ls-api-call! :editor.getTag title-ident)
|
||||
tag4 (ls-api-call! :editor.getTag (get tag1 "uuid"))]
|
||||
(is (= (get tag1 "ident") title-ident) "create tag with title from test as plugin")
|
||||
(is (= (get tag2 "ident") title-ident) "get tag with title")
|
||||
(is (= (get tag3 "title") title) "get tag with ident")
|
||||
(is (= (get tag4 "title") title) "get tag with uuid")))
|
||||
|
||||
(testing "create tag with tagProperties"
|
||||
(let [tag-props [{:name "prop1"}
|
||||
{:name "prop2" :schema {:type "number"}}
|
||||
{:name "prop3" :schema {:type "checkbox"}}]
|
||||
tag (ls-api-call! :editor.createTag "tag-with-props" {:tagProperties tag-props})
|
||||
props (get tag ":logseq.property.class/properties")]
|
||||
(is (= 3 (count props)) "tag has 3 properties")))
|
||||
|
||||
(testing "add and remove tag extends"
|
||||
(let [tag1 (ls-api-call! :editor.createTag "tag1")
|
||||
tag2 (ls-api-call! :editor.createTag "tag2")
|
||||
tag3 (ls-api-call! :editor.createTag "tag3")
|
||||
id1 (get tag1 "id")
|
||||
id2 (get tag2 "id")
|
||||
id3 (get tag3 "id")
|
||||
_ (ls-api-call! :editor.addTagExtends id1 id2)
|
||||
tag1 (ls-api-call! :editor.getTag id1)]
|
||||
(is (= (get tag1 ":logseq.property.class/extends") [id2]) "tag1 extends tag2 with db id")
|
||||
(let [_ (ls-api-call! :editor.addTagExtends id1 id3)
|
||||
tag1 (ls-api-call! :editor.getTag id1)]
|
||||
(is (= (get tag1 ":logseq.property.class/extends") [id2 id3]) "tag1 extends tag2,tag3 with db ids")))))
|
||||
|
||||
(deftest get-tags-by-name-test
|
||||
(testing "get tags by exact name"
|
||||
(let [tag-name "product"
|
||||
tag1 (ls-api-call! :editor.createTag tag-name)
|
||||
result (ls-api-call! :editor.getTagsByName tag-name)]
|
||||
(is (= 1 (count result)) "should return exactly one tag")
|
||||
(is (= (get tag1 "uuid") (get (first result) "uuid")) "should return the created tag")
|
||||
(is (= (get tag1 "title") (get (first result) "title")) "tag title should match")))
|
||||
|
||||
(testing "get tags by name is case-insensitive"
|
||||
(let [tag-name "TestTag123"
|
||||
_ (ls-api-call! :editor.createTag tag-name)
|
||||
result-lower (ls-api-call! :editor.getTagsByName "testtag123")
|
||||
result-upper (ls-api-call! :editor.getTagsByName "TESTTAG123")
|
||||
result-mixed (ls-api-call! :editor.getTagsByName "TeStTaG123")]
|
||||
(is (= 1 (count result-lower)) "should find tag with lowercase search")
|
||||
(is (= 1 (count result-upper)) "should find tag with uppercase search")
|
||||
(is (= 1 (count result-mixed)) "should find tag with mixed case search")
|
||||
(is (= (get (first result-lower) "uuid") (get (first result-upper) "uuid")) "all searches should return same tag")
|
||||
(is (= (get (first result-lower) "uuid") (get (first result-mixed) "uuid")) "all searches should return same tag")))
|
||||
|
||||
(testing "get tags by name returns empty array for non-existent tag"
|
||||
(let [result (ls-api-call! :editor.getTagsByName "NonExistentTag12345")]
|
||||
(is (empty? result) "should return empty array for non-existent tag")))
|
||||
|
||||
(testing "get tags by name filters out non-tag pages"
|
||||
(let [page-name "regular-page"
|
||||
_ (page/new-page page-name)
|
||||
result (ls-api-call! :editor.getTagsByName page-name)]
|
||||
(is (empty? result) "should not return regular pages, only tags")))
|
||||
|
||||
(testing "get tags by name with multiple tags having similar names"
|
||||
(let [_tag1 (ls-api-call! :editor.createTag "category")
|
||||
_tag2 (ls-api-call! :editor.createTag "Category")
|
||||
result (ls-api-call! :editor.getTagsByName "category")]
|
||||
;; Due to case-insensitive name normalization, both tags should be the same
|
||||
(is (>= (count result) 1) "should return at least one tag")
|
||||
;; Verify the result contains valid tag structure
|
||||
(is (string? (get (first result) "uuid")) "returned tag should have uuid")
|
||||
(is (string? (get (first result) "title")) "returned tag should have title"))))
|
||||
|
||||
(deftest set-property-node-tags
|
||||
(testing "set property node tags"
|
||||
(let [property-name (new-property)
|
||||
_ (ls-api-call! :editor.upsertProperty property-name {:type "node"})
|
||||
tag1 (ls-api-call! :editor.createTag "Tag A")
|
||||
tag2 (ls-api-call! :editor.createTag "Tag B")
|
||||
tags [(get tag1 "id") (get tag2 "id")]
|
||||
_ (ls-api-call! :editor.setPropertyNodeTags property-name tags)
|
||||
property (ls-api-call! :editor.getProperty property-name)
|
||||
node-tags (get property ":logseq.property/classes")]
|
||||
(is (= (set node-tags) (set tags)) "property node tags should match the set tags"))))
|
||||
199
clj-e2e/test/logseq/e2e/plugins_marketplace_test.clj
Normal file
199
clj-e2e/test/logseq/e2e/plugins_marketplace_test.clj
Normal file
@@ -0,0 +1,199 @@
|
||||
(ns logseq.e2e.plugins-marketplace-test
|
||||
(:require [logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.util :as util]
|
||||
[clojure.test :refer [deftest testing use-fixtures]]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each fixtures/new-logseq-page)
|
||||
|
||||
(defn- open-plugins-dialog
|
||||
"Opens the plugins dialog via the More menu"
|
||||
[]
|
||||
(util/double-esc)
|
||||
(w/click ".toolbar-dots-btn")
|
||||
(w/click ".ui__dropdown-menu-item:has-text('Plugins')")
|
||||
(w/wait-for ".cp__plugins-page"))
|
||||
|
||||
(defn- switch-to-marketplace
|
||||
"Switches to the Marketplace tab in the plugins dialog"
|
||||
[]
|
||||
(w/click "button:has-text('Marketplace')")
|
||||
;; Wait for plugins to load from marketplace
|
||||
(w/wait-for ".cp__plugins-marketplace-cnt" {:timeout 15000}))
|
||||
|
||||
(defn- search-plugin
|
||||
"Search for a plugin by name"
|
||||
[search-term]
|
||||
(w/fill ".cp__plugins-page input[placeholder*='Search']" search-term))
|
||||
|
||||
(defn- click-install-button
|
||||
"Clicks the installation button for the first visible plugin card"
|
||||
[]
|
||||
(let [install-btn (.first (w/-query ".cp__plugins-item-card .ctl a.btn:has-text('Install')"))]
|
||||
(w/click install-btn)))
|
||||
|
||||
(defn- wait-for-plugin-installed
|
||||
"Waits for the plugin to show as installed"
|
||||
[]
|
||||
(w/wait-for ".cp__plugins-item-card .ctl a.btn:has-text('Installed')" {:timeout 30000}))
|
||||
|
||||
(defn- switch-to-installed
|
||||
"Switches to the Installed tab in the plugins dialog"
|
||||
[]
|
||||
(w/click "button:has-text('Installed')")
|
||||
;; Wait for installed plugins view
|
||||
(w/wait-for ".cp__plugins-installed"))
|
||||
|
||||
(defn- close-plugins-dialog
|
||||
"Closes the plugins dialog"
|
||||
[]
|
||||
(k/esc))
|
||||
|
||||
(defn- ensure-journals-calendar-installed!
|
||||
[]
|
||||
(open-plugins-dialog)
|
||||
(switch-to-marketplace)
|
||||
(search-plugin "Journals calendar")
|
||||
(w/wait-for ".cp__plugins-item-card h3:has-text('Journals calendar')" {:timeout 10000})
|
||||
(when (w/visible? ".cp__plugins-item-card .ctl a.btn:has-text('Install')")
|
||||
(click-install-button)
|
||||
(wait-for-plugin-installed))
|
||||
(switch-to-installed)
|
||||
(search-plugin "Journals calendar")
|
||||
(w/wait-for ".cp__plugins-item-card h3:has-text('Journals calendar')"))
|
||||
|
||||
(defn- journals-calendar-card
|
||||
[]
|
||||
(.first
|
||||
(w/-query
|
||||
".cp__plugins-item-card:has(h3:has-text('Journals calendar'))")))
|
||||
|
||||
(defn- set-journals-calendar-enabled!
|
||||
[enabled?]
|
||||
(let [card (journals-calendar-card)
|
||||
toggle (.locator card "button[role='switch']")]
|
||||
(when (not= enabled? (= "true" (.getAttribute toggle "aria-checked")))
|
||||
(w/click toggle))
|
||||
(assert/assert-is-visible card)))
|
||||
|
||||
(deftest marketplace-tabs-search-and-state-test
|
||||
(testing "installed and marketplace tabs keep their own filters and visible results"
|
||||
(open-plugins-dialog)
|
||||
(assert/assert-is-visible "button:has-text('Installed')")
|
||||
(assert/assert-is-visible "button:has-text('Marketplace')")
|
||||
(switch-to-marketplace)
|
||||
(assert/assert-is-visible "button:has-text('Plugins')")
|
||||
(assert/assert-is-visible "button:has-text('Themes')")
|
||||
(search-plugin "Journals calendar")
|
||||
(w/wait-for
|
||||
".cp__plugins-item-card h3:has-text('Journals calendar')"
|
||||
{:timeout 10000})
|
||||
(w/click "button:has-text('Themes')")
|
||||
(assert/assert-is-hidden
|
||||
".cp__plugins-item-card h3:has-text('Journals calendar')")
|
||||
(w/click "button:has-text('Plugins')")
|
||||
(assert/assert-is-visible
|
||||
".cp__plugins-item-card h3:has-text('Journals calendar')")
|
||||
(switch-to-installed)
|
||||
(assert/assert-is-visible ".cp__plugins-installed")
|
||||
(switch-to-marketplace)
|
||||
(assert/assert-is-visible
|
||||
".cp__plugins-item-card h3:has-text('Journals calendar')")
|
||||
(close-plugins-dialog)
|
||||
(assert/assert-is-hidden ".cp__plugins-page")))
|
||||
|
||||
(deftest install-plugin-from-marketplace
|
||||
(testing "Install a plugin from the marketplace"
|
||||
;; Open plugins dialog
|
||||
(open-plugins-dialog)
|
||||
|
||||
;; Switch to marketplace tab
|
||||
(switch-to-marketplace)
|
||||
|
||||
;; Search for a specific plugin (using Journals calendar as it's a well-known plugin)
|
||||
(search-plugin "Journals calendar")
|
||||
|
||||
;; Wait for search results
|
||||
(w/wait-for ".cp__plugins-item-card h3:has-text('Journals calendar')" {:timeout 10000})
|
||||
|
||||
;; Click install on the plugin
|
||||
(click-install-button)
|
||||
|
||||
;; Wait for installation to complete
|
||||
(wait-for-plugin-installed)
|
||||
|
||||
;; Verify the plugin is now marked as installed in marketplace
|
||||
(assert/assert-is-visible ".cp__plugins-item-card .ctl a.btn.disabled:has-text('Installed')")
|
||||
|
||||
;; Switch to installed tab and verify plugin appears there
|
||||
(switch-to-installed)
|
||||
(assert/assert-is-visible ".cp__plugins-item-card h3:has-text('Journals calendar')")
|
||||
|
||||
;; Close the dialog
|
||||
(close-plugins-dialog)
|
||||
|
||||
;; Check that the plugin is active in the main UI (e.g., check for a UI element added by the plugin
|
||||
(w/wait-for ".toolbar-plugins-manager-trigger")
|
||||
(w/click ".toolbar-plugins-manager-trigger")
|
||||
(assert/assert-is-visible "a.button[data-on-click=goToToday]")
|
||||
(w/click ".ui__dropdown-menu-content a.button[data-on-click=goToToday]")
|
||||
(assert/assert-is-visible ".is-today-page")))
|
||||
|
||||
(deftest plugin-command-registration-follows-lifecycle-test
|
||||
(testing "a plugin command is registered once, removed on disable, and restored on enable"
|
||||
(ensure-journals-calendar-installed!)
|
||||
(set-journals-calendar-enabled! true)
|
||||
(close-plugins-dialog)
|
||||
(w/click ".toolbar-plugins-manager-trigger")
|
||||
(assert/assert-have-count
|
||||
".ui__dropdown-menu-content a.button[data-on-click=goToToday]"
|
||||
1)
|
||||
(k/esc)
|
||||
|
||||
(open-plugins-dialog)
|
||||
(switch-to-installed)
|
||||
(search-plugin "Journals calendar")
|
||||
(set-journals-calendar-enabled! false)
|
||||
(close-plugins-dialog)
|
||||
(w/click ".toolbar-plugins-manager-trigger")
|
||||
(assert/assert-have-count
|
||||
".ui__dropdown-menu-content a.button[data-on-click=goToToday]"
|
||||
0)
|
||||
(k/esc)
|
||||
|
||||
(open-plugins-dialog)
|
||||
(switch-to-installed)
|
||||
(search-plugin "Journals calendar")
|
||||
(set-journals-calendar-enabled! true)
|
||||
(close-plugins-dialog)
|
||||
(w/click ".toolbar-plugins-manager-trigger")
|
||||
(assert/assert-have-count
|
||||
".ui__dropdown-menu-content a.button[data-on-click=goToToday]"
|
||||
1)))
|
||||
|
||||
(deftest plugin-disable-enable-and-reload-test
|
||||
(testing "plugin UI contributions are completely removed and restored without duplicates"
|
||||
(ensure-journals-calendar-installed!)
|
||||
(set-journals-calendar-enabled! false)
|
||||
(close-plugins-dialog)
|
||||
(assert/assert-have-count "a.button[data-on-click=goToToday]" 0)
|
||||
|
||||
(open-plugins-dialog)
|
||||
(switch-to-installed)
|
||||
(search-plugin "Journals calendar")
|
||||
(set-journals-calendar-enabled! true)
|
||||
(close-plugins-dialog)
|
||||
(w/click ".toolbar-plugins-manager-trigger")
|
||||
(assert/assert-have-count
|
||||
".ui__dropdown-menu-content a.button[data-on-click=goToToday]"
|
||||
1)
|
||||
(k/esc)
|
||||
|
||||
(util/refresh-until-graph-loaded)
|
||||
(w/click ".toolbar-plugins-manager-trigger")
|
||||
(assert/assert-have-count
|
||||
".ui__dropdown-menu-content a.button[data-on-click=goToToday]"
|
||||
1)))
|
||||
95
clj-e2e/test/logseq/e2e/property_basic_test.clj
Normal file
95
clj-e2e/test/logseq/e2e/property_basic_test.clj
Normal file
@@ -0,0 +1,95 @@
|
||||
(ns logseq.e2e.property-basic-test
|
||||
(:require [clojure.test :refer [deftest use-fixtures]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(def ^:private property-types ["Text" "Number" "Date" "DateTime" "Checkbox" "URL" "Node"])
|
||||
|
||||
(defn add-new-properties
|
||||
[title-prefix]
|
||||
(b/new-blocks (map #(str title-prefix "-" %) property-types))
|
||||
(doseq [property-type property-types]
|
||||
(let [property-name (str "p-" title-prefix "-" property-type)]
|
||||
(w/click (util/get-by-text (str title-prefix "-" property-type) true))
|
||||
(k/press "Control+e")
|
||||
(util/input-command "Add property")
|
||||
(w/click "input[placeholder]")
|
||||
(util/input property-name)
|
||||
(w/click (util/get-by-text "New option:" false))
|
||||
(assert/assert-is-visible (w/get-by-text "Select a property type"))
|
||||
(w/click (loc/and "span" (util/get-by-text property-type true)))
|
||||
(case property-type
|
||||
"Text" (do
|
||||
(w/click (format ".property-pair:has-text('%s') > .ls-block" property-name))
|
||||
(util/input "Text"))
|
||||
"Number" (do (assert/assert-is-visible (format "input[placeholder='%s']" (str "Set " property-name)))
|
||||
(util/input "111")
|
||||
(w/click (w/get-by-text "New option:")))
|
||||
("DateTime" "Date") (do
|
||||
(assert/assert-is-visible ".ls-property-dialog")
|
||||
(k/enter)
|
||||
(k/esc))
|
||||
"Checkbox" nil
|
||||
"URL" nil
|
||||
"Node" (do
|
||||
(w/click (w/get-by-text "Skip choosing tag"))
|
||||
(util/input (str title-prefix "-Node-value"))
|
||||
(w/click (w/get-by-text "New option:")))))))
|
||||
|
||||
(deftest new-property-test
|
||||
(let [title-prefix "new-property-test"]
|
||||
(add-new-properties title-prefix)))
|
||||
|
||||
(deftest property-value-lifecycle-and-object-view-persistence-test
|
||||
(let [property-name "property-value-lifecycle"
|
||||
target-title "property value target"
|
||||
owner-page (page/get-page-name)]
|
||||
(b/new-block target-title)
|
||||
(util/input-command "Add property")
|
||||
(w/click "input[placeholder]")
|
||||
(util/input property-name)
|
||||
(w/click (w/get-by-text "New option:"))
|
||||
(w/click (loc/and "span" (util/get-by-text "Text" true)))
|
||||
(w/click (format ".property-pair:has-text('%s') > .ls-block" property-name))
|
||||
(util/input "Initial value")
|
||||
(k/esc)
|
||||
(assert/assert-is-visible
|
||||
(format ".property-pair:has-text('%s'):has-text('Initial value')" property-name))
|
||||
|
||||
(page/goto-page property-name)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body" :has-text target-title))
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body" :has-text target-title))
|
||||
|
||||
(page/goto-page owner-page)
|
||||
(w/click (format ".property-pair:has-text('%s') > .ls-block" property-name))
|
||||
(util/input "Updated value")
|
||||
(k/esc)
|
||||
(assert/assert-is-visible
|
||||
(format ".property-pair:has-text('%s'):has-text('Updated value')" property-name))
|
||||
|
||||
(w/click (loc/filter ".property-k" :has-text property-name))
|
||||
(w/click (loc/filter "[role='menuitem']" :has-text "Delete property from node"))
|
||||
(w/click "div[role='alertdialog'] button:text('Confirm')")
|
||||
(assert/assert-have-count
|
||||
(format ".property-pair:has-text('%s')" property-name)
|
||||
0)
|
||||
|
||||
(page/goto-page property-name)
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".ls-view-body" :has-text target-title)
|
||||
0)))
|
||||
106
clj-e2e/test/logseq/e2e/property_config_basic_test.clj
Normal file
106
clj-e2e/test/logseq/e2e/property_config_basic_test.clj
Normal file
@@ -0,0 +1,106 @@
|
||||
(ns logseq.e2e.property-config-basic-test
|
||||
(:require [clojure.test :refer [deftest use-fixtures]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(defn- add-text-property
|
||||
[property-name]
|
||||
(b/new-block "property target")
|
||||
(util/input-command "Add property")
|
||||
(w/click "input[placeholder]")
|
||||
(util/input property-name)
|
||||
(w/click (w/get-by-text "New option:"))
|
||||
(w/click (loc/and "span" (util/get-by-text "Text" true)))
|
||||
(k/esc)
|
||||
(assert/assert-is-visible (format ".property-k:text('%s')" property-name)))
|
||||
|
||||
(defn- open-choices-pane
|
||||
[property-name]
|
||||
(w/click (loc/filter ".property-k" :has-text property-name))
|
||||
(w/click (loc/filter "div[role='menuitem']" :has-text "Available choices")))
|
||||
|
||||
(defn- add-choice
|
||||
[choice]
|
||||
(w/click (loc/filter "div[role='menuitem']" :has-text "Add choice"))
|
||||
(w/fill "input[placeholder='title']" choice)
|
||||
(w/click "button:has-text('Save')")
|
||||
(assert/assert-is-visible (format ".choices-list li:has-text('%s')" choice)))
|
||||
|
||||
(deftest property-choices-configuration-and-mod-p-stay-reactive-test
|
||||
(let [property-name "reactive-priority"
|
||||
choice-before "Choice before"
|
||||
choice-after "Choice after"
|
||||
removable-choice "Choice to delete"]
|
||||
(add-text-property property-name)
|
||||
(open-choices-pane property-name)
|
||||
|
||||
(add-choice choice-before)
|
||||
(w/click (format ".choices-list li:has-text('%s') strong" choice-before))
|
||||
(w/fill "input[placeholder='title']" choice-after)
|
||||
(w/click "button:has-text('Save')")
|
||||
(assert/assert-is-visible (format ".choices-list li:has-text('%s')" choice-after))
|
||||
(assert/assert-have-count (format ".choices-list li:has-text('%s')" choice-before) 0)
|
||||
|
||||
(w/click
|
||||
(format ".choices-list li:has-text('%s') button[title='More settings']"
|
||||
choice-after))
|
||||
(w/click (loc/filter "div[role='menuitem']" :has-text "Set as default choice"))
|
||||
|
||||
(open-choices-pane property-name)
|
||||
(add-choice removable-choice)
|
||||
(w/click
|
||||
(format ".choices-list li:has-text('%s') button[title='More settings']"
|
||||
removable-choice))
|
||||
(w/click "div[role='menuitem'].del")
|
||||
(assert/assert-have-count
|
||||
(format ".choices-list li:has-text('%s')" removable-choice)
|
||||
0)
|
||||
|
||||
(util/double-esc)
|
||||
(b/new-block "closed choice target")
|
||||
(k/press (if util/mac? "ControlOrMeta+p" "Control+Alt+p"))
|
||||
(w/fill ".ls-property-dialog .cp__select-input" property-name)
|
||||
(w/click (loc/filter "a.menu-link" :has-text property-name))
|
||||
(assert/assert-is-visible
|
||||
(format ".ls-property-dialog input[placeholder='Set %s']" property-name))
|
||||
(assert/assert-have-count ".ls-property-dialog :text('Empty')" 0)
|
||||
(w/click
|
||||
(loc/filter ".ls-property-dialog .cp__select-results" :has-text choice-after))
|
||||
(assert/assert-is-visible
|
||||
(format ".ls-block :text('%s')" choice-after))))
|
||||
|
||||
(deftest mod-p-creates-and-sets-text-property-test
|
||||
(let [property-name "mod-p-text"
|
||||
property-value "created from mod p"]
|
||||
(b/new-block "mod p target")
|
||||
(k/press (if util/mac? "ControlOrMeta+p" "Control+Alt+p"))
|
||||
(w/fill ".ls-property-dialog .cp__select-input" property-name)
|
||||
(w/click (w/get-by-text "New option:"))
|
||||
(w/click (loc/and "span" (util/get-by-text "Text" true)))
|
||||
(assert/assert-is-visible
|
||||
(format ".property-pair:has-text('%s') textarea" property-name))
|
||||
(util/input property-value)
|
||||
(k/esc)
|
||||
(assert/assert-is-visible (format ".property-k:text('%s')" property-name))
|
||||
(assert/assert-is-visible
|
||||
(format ".property-pair:has-text('%s'):has-text('%s')" property-name property-value))))
|
||||
|
||||
(deftest property-table-hides-internal-id-column-test
|
||||
(let [property-name "table-without-internal-id"]
|
||||
(add-text-property property-name)
|
||||
(util/double-esc)
|
||||
(page/goto-page property-name)
|
||||
(assert/assert-is-visible ".ls-view-body .ls-table-header-cell")
|
||||
(assert/assert-have-count ".ls-view-body .ls-table-header-cell:text('#')" 0)))
|
||||
131
clj-e2e/test/logseq/e2e/property_scoped_choices_test.clj
Normal file
131
clj-e2e/test/logseq/e2e/property_scoped_choices_test.clj
Normal file
@@ -0,0 +1,131 @@
|
||||
(ns logseq.e2e.property-scoped-choices-test
|
||||
(:require [clojure.test :refer [deftest use-fixtures]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]
|
||||
[wally.repl :as repl]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(defn- add-property
|
||||
[property-name]
|
||||
(b/new-blocks ["setup"])
|
||||
(w/click (util/get-by-text "setup" true))
|
||||
(k/press "Control+e")
|
||||
(util/input-command "Add property")
|
||||
(w/click "input[placeholder]")
|
||||
(util/input property-name)
|
||||
(w/click (w/get-by-text "New option:"))
|
||||
(w/click (loc/and "span" (util/get-by-text "Text" true)))
|
||||
(k/esc)
|
||||
(assert/assert-is-visible (format ".property-k:text('%s')" property-name)))
|
||||
|
||||
(defn- add-tag-property
|
||||
[property-name]
|
||||
(w/click "button:has-text('Add tag property')")
|
||||
(w/click "input[placeholder='Add or change property']")
|
||||
(util/input property-name)
|
||||
(w/click (loc/filter "a.menu-link" :has-text property-name))
|
||||
(assert/assert-is-visible (format ".property-k:text('%s')" property-name)))
|
||||
|
||||
(defn- open-choices-pane
|
||||
[property-name]
|
||||
(w/click (loc/filter ".property-k" :has-text property-name))
|
||||
(w/click (loc/filter "div[role='menuitem']" :has-text "Available choices")))
|
||||
|
||||
(defn- add-choice
|
||||
[property-name choice]
|
||||
(open-choices-pane property-name)
|
||||
(w/click (loc/filter "div[role='menuitem']" :has-text "Add choice"))
|
||||
(w/fill "input[placeholder='title']" choice)
|
||||
(w/click "button:has-text('Save')")
|
||||
(k/esc))
|
||||
|
||||
(defn- hide-choice-for-tag
|
||||
[property-name choice tag]
|
||||
(open-choices-pane property-name)
|
||||
(util/wait-timeout 100)
|
||||
(w/click (format ".choices-list li:has-text('%s') button[title='More settings']" choice))
|
||||
(util/wait-timeout 100)
|
||||
(w/click (loc/filter "div[role='menuitem']" :has-text (str "Hide for #" tag)))
|
||||
(k/esc))
|
||||
|
||||
(defn- open-property-value-select
|
||||
[property-name]
|
||||
(w/click (format ".bottom-property-pill:has(.property-k:has-text('%s')) .property-value-container .jtrigger"
|
||||
property-name))
|
||||
(assert/assert-is-visible (format "input[placeholder='Set %s']" property-name))
|
||||
(w/click (format "input[placeholder='Set %s']" property-name))
|
||||
(assert/assert-is-visible ".cp__select-results"))
|
||||
|
||||
(deftest tag-scoped-property-choices-test
|
||||
(let [setup-page (page/get-page-name)
|
||||
tag "Device"
|
||||
property-name "device-type"
|
||||
scoped-choice "wired"
|
||||
global-choice "wireless"]
|
||||
(add-property property-name)
|
||||
(page/new-page tag)
|
||||
(page/convert-to-tag tag)
|
||||
(add-tag-property property-name)
|
||||
(add-choice property-name scoped-choice)
|
||||
(util/wait-timeout 100)
|
||||
(k/esc)
|
||||
(page/goto-page setup-page)
|
||||
(add-choice property-name global-choice)
|
||||
(util/wait-timeout 100)
|
||||
(k/esc)
|
||||
(page/goto-page tag)
|
||||
;; open tag properties
|
||||
(w/click (.first (w/-query "a.block-control")))
|
||||
(hide-choice-for-tag property-name global-choice tag)
|
||||
(util/wait-timeout 100)
|
||||
(k/esc)
|
||||
(page/new-page "scoped-choices-test")
|
||||
(b/new-block "Device item")
|
||||
(util/set-tag tag)
|
||||
(open-property-value-select property-name)
|
||||
(assert/assert-is-visible (loc/filter ".cp__select-results" :has-text scoped-choice))
|
||||
(assert/assert-have-count (loc/filter ".cp__select-results" :has-text global-choice) 0)))
|
||||
|
||||
(deftest tag-scoped-property-choices-isolated-test
|
||||
(let [tag-a "Device2"
|
||||
tag-b "Vehicle"
|
||||
property-name "device2-type"
|
||||
choice-a "wired2"
|
||||
choice-b "gas"]
|
||||
(add-property property-name)
|
||||
(page/new-page tag-a)
|
||||
(page/convert-to-tag tag-a)
|
||||
(add-tag-property property-name)
|
||||
(add-choice property-name choice-a)
|
||||
(util/wait-timeout 100)
|
||||
(k/esc)
|
||||
(page/new-page tag-b)
|
||||
(page/convert-to-tag tag-b)
|
||||
(add-tag-property property-name)
|
||||
(add-choice property-name choice-b)
|
||||
(util/wait-timeout 100)
|
||||
(k/esc)
|
||||
(page/new-page "scoped-choices-device2")
|
||||
(b/new-block "Device2 item")
|
||||
(util/set-tag tag-a)
|
||||
(open-property-value-select property-name)
|
||||
(assert/assert-is-visible (loc/filter ".cp__select-results" :has-text choice-a))
|
||||
(assert/assert-have-count (loc/filter ".cp__select-results" :has-text choice-b) 0)
|
||||
(k/esc)
|
||||
(page/new-page "scoped-choices-vehicle")
|
||||
(b/new-block "Vehicle item")
|
||||
(util/set-tag tag-b)
|
||||
(open-property-value-select property-name)
|
||||
(assert/assert-is-visible (loc/filter ".cp__select-results" :has-text choice-b))
|
||||
(assert/assert-have-count (loc/filter ".cp__select-results" :has-text choice-a) 0)))
|
||||
88
clj-e2e/test/logseq/e2e/reference_basic_test.clj
Normal file
88
clj-e2e/test/logseq/e2e/reference_basic_test.clj
Normal file
@@ -0,0 +1,88 @@
|
||||
(ns logseq.e2e.reference-basic-test
|
||||
(:require
|
||||
[clojure.test :refer [deftest testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
;; block references
|
||||
(deftest self-reference
|
||||
(testing "self reference"
|
||||
(b/new-block "b2")
|
||||
(b/copy)
|
||||
(b/paste)
|
||||
(util/exit-edit)
|
||||
(assert/assert-selected-block-text "b2")))
|
||||
|
||||
(deftest self-tag-block-reference
|
||||
(testing "self reference"
|
||||
(b/new-block "b2")
|
||||
(util/set-tag "task")
|
||||
(b/copy)
|
||||
(b/paste)
|
||||
(util/exit-edit)
|
||||
(assert/assert-selected-block-text "b2")))
|
||||
|
||||
(deftest mutual-reference
|
||||
(testing "mutual reference"
|
||||
(b/new-blocks ["b1" "b2"])
|
||||
(util/set-tag "task")
|
||||
(b/copy)
|
||||
(k/arrow-up)
|
||||
(b/wait-editor-text "b1")
|
||||
(b/paste)
|
||||
(b/copy)
|
||||
(k/arrow-down)
|
||||
(b/wait-editor-text "b2")
|
||||
(b/paste)
|
||||
(util/exit-edit)
|
||||
(b/assert-blocks-visible ["b1[[b2]]" "b2[[b1]]"])))
|
||||
|
||||
(deftest parent-reference
|
||||
(testing "parent reference"
|
||||
(b/new-blocks ["b1" "b2"])
|
||||
(util/set-tag "task")
|
||||
(b/indent)
|
||||
(b/copy)
|
||||
(k/arrow-up)
|
||||
(b/wait-editor-text "b1")
|
||||
(b/paste)
|
||||
(b/copy)
|
||||
(k/arrow-down)
|
||||
(b/wait-editor-text "b2")
|
||||
(b/paste)
|
||||
(util/exit-edit)
|
||||
(b/assert-blocks-visible ["b1[[b2]]" "b2[[b1]]"])))
|
||||
|
||||
(deftest cycle-reference
|
||||
(testing "cycle reference"
|
||||
(b/new-blocks ["b1" "b2" "b3"])
|
||||
(util/set-tag "task")
|
||||
(b/jump-to-block "b1")
|
||||
(assert/assert-editor-mode)
|
||||
(b/copy)
|
||||
(k/arrow-down)
|
||||
(b/wait-editor-text "b2")
|
||||
(b/paste)
|
||||
(b/copy)
|
||||
(k/arrow-down)
|
||||
(b/wait-editor-text "b3")
|
||||
(b/paste)
|
||||
(b/copy)
|
||||
(b/jump-to-block "b1")
|
||||
(assert/assert-editor-mode)
|
||||
(b/paste)
|
||||
(util/exit-edit)
|
||||
(b/assert-blocks-visible ["b1[[b3[[b2]]]]" "b2[[b1[[b3]]]]" "b3[[b2[[b1]]]]"])))
|
||||
|
||||
;; TODO: page references
|
||||
99
clj-e2e/test/logseq/e2e/right_sidebar_basic_test.clj
Normal file
99
clj-e2e/test/logseq/e2e/right_sidebar_basic_test.clj
Normal file
@@ -0,0 +1,99 @@
|
||||
(ns logseq.e2e.right-sidebar-basic-test
|
||||
(:require
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.page :as p]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
(use-fixtures :each fixtures/new-logseq-page fixtures/validate-graph)
|
||||
|
||||
(defn- use-dark-neutral-theme!
|
||||
[]
|
||||
(w/eval-js
|
||||
"window.logseq.api.set_theme_mode('dark');
|
||||
window.logseq.api.set_state_from_store(['ui/system-theme?'], false);
|
||||
window.logseq.api.set_state_from_store(['ui/radix-color'], 'none');")
|
||||
(util/wait-timeout 100))
|
||||
|
||||
(defn- right-sidebar-backgrounds
|
||||
[]
|
||||
(-> (w/eval-js
|
||||
"(() => {
|
||||
const topbar = document.querySelector('.cp__right-sidebar-topbar');
|
||||
const inner = document.querySelector('.cp__right-sidebar-inner');
|
||||
return [
|
||||
getComputedStyle(topbar).backgroundColor,
|
||||
getComputedStyle(inner).backgroundColor,
|
||||
document.documentElement.dataset.theme,
|
||||
document.documentElement.dataset.color
|
||||
].join('|');
|
||||
})()")
|
||||
(string/split #"\|")))
|
||||
|
||||
(deftest right-sidebar-topbar-uses-dark-neutral-background
|
||||
(testing "right sidebar header stays dark with neutral accent color"
|
||||
(use-dark-neutral-theme!)
|
||||
(w/click ".toggle-right-sidebar")
|
||||
(assert/assert-is-visible ".cp__right-sidebar.open .cp__right-sidebar-topbar")
|
||||
(assert/assert-is-visible ".cp__right-sidebar .sidebar-item")
|
||||
(let [[topbar-bg inner-bg theme color] (right-sidebar-backgrounds)]
|
||||
(is (= "dark" theme))
|
||||
(is (= "none" color))
|
||||
(is (= inner-bg topbar-bg)))))
|
||||
|
||||
(deftest right-sidebar-uses-only-content-scrollbar
|
||||
(testing "the outer sidebar does not reserve a second scrollbar"
|
||||
(w/click ".toggle-right-sidebar")
|
||||
(assert/assert-is-visible ".cp__right-sidebar.open .sidebar-item-list")
|
||||
(let [outer-overflow
|
||||
(w/eval-js
|
||||
"getComputedStyle(document.querySelector('.cp__right-sidebar-scrollable')).overflowY")
|
||||
content-overflow
|
||||
(w/eval-js
|
||||
"getComputedStyle(document.querySelector('.sidebar-item-list')).overflowY")]
|
||||
(is (= "visible" outer-overflow))
|
||||
(is (= "auto" content-overflow)))))
|
||||
|
||||
(deftest same-block-updates-in-main-and-right-sidebar
|
||||
(testing "one mounted UUID rerenders content and properties in both containers"
|
||||
(let [page-name (p/get-page-name)
|
||||
initial-title "sidebar live block before"
|
||||
updated-title "sidebar live block after"
|
||||
property-name "sidebar-live-property"
|
||||
initial-property-value "sidebar property before"
|
||||
updated-property-value "sidebar property after"
|
||||
block (ls-api-call! :editor.appendBlockInPage
|
||||
initial-title
|
||||
{:properties {property-name initial-property-value}})
|
||||
block-uuid (get block "uuid")
|
||||
main-block (format ".ls-page-blocks #ls-block-%s" block-uuid)
|
||||
sidebar-block (format ".cp__right-sidebar #ls-block-%s" block-uuid)]
|
||||
(w/wait-for (format "%s .block-title-wrap:text('%s')" main-block initial-title))
|
||||
(ls-api-call! :editor.openInRightSidebar block-uuid)
|
||||
(w/wait-for sidebar-block)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".cp__right-sidebar .sidebar-item-header .breadcrumb"
|
||||
:has-text page-name))
|
||||
(assert/assert-have-count (format "#ls-block-%s" block-uuid) 2)
|
||||
|
||||
(ls-api-call! :editor.updateBlock block-uuid updated-title)
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
block-uuid property-name updated-property-value)
|
||||
|
||||
(doseq [block-selector [main-block sidebar-block]]
|
||||
(w/wait-for
|
||||
(format "%s .block-title-wrap:text('%s')" block-selector updated-title))
|
||||
(w/wait-for
|
||||
(format "%s .property-k:text('%s')" block-selector property-name))
|
||||
(w/wait-for
|
||||
(format "%s .property-value :text('%s')"
|
||||
block-selector updated-property-value)))
|
||||
(w/wait-for-not-visible
|
||||
(format "#ls-block-%s .block-title-wrap:text('%s')"
|
||||
block-uuid initial-title)))))
|
||||
99
clj-e2e/test/logseq/e2e/rtc_basic_test.clj
Normal file
99
clj-e2e/test/logseq/e2e/rtc_basic_test.clj
Normal file
@@ -0,0 +1,99 @@
|
||||
(ns logseq.e2e.rtc-basic-test
|
||||
(:require
|
||||
[clojure.test :refer [deftest testing is use-fixtures]]
|
||||
[com.climate.claypoole :as cp]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.const :refer [*page1 *page2]]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.graph :as graph]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.rtc :as rtc]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]
|
||||
[wally.repl :as repl]))
|
||||
|
||||
(use-fixtures :once fixtures/open-2-pages)
|
||||
|
||||
(use-fixtures :each fixtures/validate-graph)
|
||||
|
||||
(deftest rtc-basic-test
|
||||
(let [graph-name (str "rtc-graph-" (.toEpochMilli (java.time.Instant/now)))
|
||||
page-names (map #(str "rtc-test-page" %) (range 4))]
|
||||
(testing "open 2 app instances, add a rtc graph, check this graph available on other instance"
|
||||
(cp/prun!
|
||||
2
|
||||
#(w/with-page %
|
||||
(util/login-test-account))
|
||||
[@*page1 @*page2])
|
||||
(testing "remote graph refresh waits until the button is enabled"
|
||||
(w/with-page @*page2
|
||||
(graph/goto-all-graphs)
|
||||
(w/wait-for "button:not([disabled]):has-text(\"Refresh\")" {:timeout 30000})
|
||||
(.setDefaultTimeout (w/get-page) 50)
|
||||
(try
|
||||
(w/eval-js
|
||||
"() => {
|
||||
const span = Array.from(document.querySelectorAll('span'))
|
||||
.find((node) => node.textContent.trim() === 'Refresh');
|
||||
const button = span.closest('button');
|
||||
button.disabled = true;
|
||||
setTimeout(() => { button.disabled = false; }, 500);
|
||||
}")
|
||||
(graph/refresh-all-remote-graphs)
|
||||
(finally
|
||||
(.setDefaultTimeout (w/get-page) 10000)))))
|
||||
(w/with-page @*page1
|
||||
(graph/new-graph graph-name true false))
|
||||
(w/with-page @*page2
|
||||
(graph/wait-for-remote-graph graph-name)
|
||||
(graph/switch-graph graph-name true true)))
|
||||
(testing "logseq pages add/delete"
|
||||
(doseq [page-name page-names]
|
||||
(let [{:keys [_local-tx remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(page/new-page page-name)))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx)
|
||||
(util/search-and-click page-name))))
|
||||
(let [*last-remote-tx (atom nil)]
|
||||
(doseq [page-name page-names]
|
||||
(let [{:keys [_local-tx remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(page/delete-page page-name)))]
|
||||
(reset! *last-remote-tx remote-tx)))
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to @*last-remote-tx)
|
||||
(doseq [page-name page-names]
|
||||
(let [deleted-page (ls-api-call! :editor.getPage page-name)]
|
||||
(is (number? (get deleted-page ":logseq.property/deleted-at")))
|
||||
(is (= "Recycle" (get-in deleted-page ["parent" "title"]))))))))
|
||||
(testing "Page reference created"
|
||||
(let [page-name "test-page-reference"
|
||||
{:keys [_local-tx remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(page/new-page page-name)))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx)))
|
||||
(let [test-page (str "random page " (random-uuid))
|
||||
block-title (format "test ref [[%s]]" test-page)
|
||||
{:keys [_local-tx remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(b/new-block block-title)
|
||||
(b/new-block "add new-block to ensure last block saved")))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx)
|
||||
(util/search-and-click test-page)
|
||||
(w/wait-for ".references .ls-block")
|
||||
;; ensure ref exists
|
||||
(let [refs (w/all-text-contents ".references .ls-block .block-title-wrap")]
|
||||
(is (= refs [block-title]))))))
|
||||
|
||||
(testing "cleanup"
|
||||
(w/with-page @*page2
|
||||
(graph/remove-remote-graph graph-name)))))
|
||||
461
clj-e2e/test/logseq/e2e/rtc_extra_part2_test.clj
Normal file
461
clj-e2e/test/logseq/e2e/rtc_extra_part2_test.clj
Normal file
@@ -0,0 +1,461 @@
|
||||
(ns logseq.e2e.rtc-extra-part2-test
|
||||
(:require [clojure.java.io :as io]
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest testing is use-fixtures]]
|
||||
[jsonista.core :as json]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.const :refer [*page1 *page2 *graph-name*]]
|
||||
[logseq.e2e.custom-report :as custom-report]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.graph :as graph]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.rtc :as rtc]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once
|
||||
fixtures/open-2-pages
|
||||
(partial fixtures/prepare-rtc-graph-fixture "rtc-extra-part2-test-graph"))
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page-in-rtc)
|
||||
|
||||
(def ^:private stress-default-rounds 1)
|
||||
(def ^:private stress-default-ops-per-client 50)
|
||||
(def ^:private stress-default-seed-blocks 20)
|
||||
(def ^:private stress-default-seed 20260330)
|
||||
(def ^:private stress-max-seed-depth 4)
|
||||
(def ^:private severe-sync-log-patterns
|
||||
["db-sync/checksum-mismatch"
|
||||
"db-sync/tx-rejected"
|
||||
"db-sync/apply-remote-txs-failed"])
|
||||
(def ^:private random-edit-actions
|
||||
[:new :save :indent-outdent :delete-existing :undo :redo])
|
||||
|
||||
(defn- env-int
|
||||
[k default]
|
||||
(let [raw (System/getenv k)]
|
||||
(if-not (string/blank? raw)
|
||||
(try
|
||||
(Integer/parseInt raw)
|
||||
(catch Throwable _
|
||||
default))
|
||||
default)))
|
||||
|
||||
(defn- recent-console-logs
|
||||
[]
|
||||
(->> (some-> custom-report/*pw-page->console-logs* deref vals)
|
||||
(mapcat identity)
|
||||
vec))
|
||||
|
||||
(defn- assert-no-severe-sync-errors!
|
||||
[]
|
||||
(let [logs (recent-console-logs)
|
||||
matched (->> logs
|
||||
(filter (fn [line]
|
||||
(some #(string/includes? line %) severe-sync-log-patterns)))
|
||||
vec)]
|
||||
(is (empty? matched)
|
||||
(str "found severe sync errors in console logs: "
|
||||
(pr-str (take-last 20 matched))))))
|
||||
|
||||
(defn- page-sync-state
|
||||
[pw-page]
|
||||
(w/with-page pw-page
|
||||
(util/exit-edit)
|
||||
{:rtc-tx (rtc/get-rtc-tx)
|
||||
:blocks (util/get-page-blocks-contents)}))
|
||||
|
||||
(defn- assert-two-pages-synced!
|
||||
[]
|
||||
(let [s1 (page-sync-state @*page1)
|
||||
s2 (page-sync-state @*page2)
|
||||
tx1 (:rtc-tx s1)
|
||||
tx2 (:rtc-tx s2)]
|
||||
(is (= (:blocks s1) (:blocks s2))
|
||||
(str "page blocks diverged: "
|
||||
(pr-str {:page1-count (count (:blocks s1))
|
||||
:page2-count (count (:blocks s2))
|
||||
:page1-tail (take-last 8 (:blocks s1))
|
||||
:page2-tail (take-last 8 (:blocks s2))})))
|
||||
(is (= (:local-tx tx1) (:remote-tx tx1))
|
||||
(str "page1 rtc-tx not converged: " (pr-str tx1)))
|
||||
(is (= (:local-tx tx2) (:remote-tx tx2))
|
||||
(str "page2 rtc-tx not converged: " (pr-str tx2)))))
|
||||
|
||||
(defn- try-indent!
|
||||
[]
|
||||
(if-let [editor (util/get-editor)]
|
||||
(let [[x1 _] (util/bounding-xy editor)]
|
||||
(k/tab)
|
||||
(if-let [editor' (util/get-editor)]
|
||||
(let [[x2 _] (util/bounding-xy editor')]
|
||||
(> x2 x1))
|
||||
false))
|
||||
false))
|
||||
|
||||
(defn- try-outdent!
|
||||
[]
|
||||
(if-let [editor (util/get-editor)]
|
||||
(let [[x1 _] (util/bounding-xy editor)]
|
||||
(k/shift+tab)
|
||||
(if-let [editor' (util/get-editor)]
|
||||
(let [[x2 _] (util/bounding-xy editor')]
|
||||
(> x1 x2))
|
||||
false))
|
||||
false))
|
||||
|
||||
(defn- align-depth!
|
||||
[depth target]
|
||||
(loop [d depth]
|
||||
(cond
|
||||
(< d target) (if (try-indent!)
|
||||
(recur (inc d))
|
||||
d)
|
||||
(> d target) (if (try-outdent!)
|
||||
(recur (dec d))
|
||||
d)
|
||||
:else d)))
|
||||
|
||||
(defn- new-block-safe!
|
||||
[title]
|
||||
(loop [attempt 4]
|
||||
(let [created?
|
||||
(try
|
||||
(b/new-block "")
|
||||
(b/save-block title)
|
||||
true
|
||||
(catch Throwable _
|
||||
false))]
|
||||
(if created?
|
||||
true
|
||||
(if (zero? attempt)
|
||||
(throw (ex-info "new-block-safe failed" {:title title}))
|
||||
(do
|
||||
(util/exit-edit)
|
||||
(util/wait-timeout 80)
|
||||
(try
|
||||
(b/open-last-block)
|
||||
(catch Throwable _
|
||||
nil))
|
||||
(util/wait-timeout 80)
|
||||
(recur (dec attempt))))))))
|
||||
|
||||
(defn- sync-by-trigger!
|
||||
([tag]
|
||||
(sync-by-trigger! tag nil))
|
||||
([tag checkpoints]
|
||||
(let [target-tx (some->> checkpoints
|
||||
vals
|
||||
(filter integer?)
|
||||
seq
|
||||
(apply max))]
|
||||
;; Ensure both pages have observed all prior edit/undo-redo txs first.
|
||||
(when target-tx
|
||||
(w/with-page @*page1
|
||||
(rtc/wait-tx-update-to target-tx))
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to target-tx)))
|
||||
(let [{:keys [remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(new-block-safe! (str "sync-trigger-" tag))))]
|
||||
(w/with-page @*page1
|
||||
(rtc/wait-tx-update-to remote-tx))
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx))))))
|
||||
|
||||
(defn- seed-long-nested-page!
|
||||
[seed]
|
||||
(let [seed-blocks (max 20 (env-int "DB_SYNC_E2E_STRESS_SEED_BLOCKS" stress-default-seed-blocks))
|
||||
rng (java.util.Random. (long (+ seed 97)))
|
||||
titles (w/with-page @*page1
|
||||
(util/exit-edit)
|
||||
(loop [i 0
|
||||
depth 0
|
||||
titles #{}]
|
||||
(if (< i seed-blocks)
|
||||
(let [title (format "seed-r%s-%03d" seed i)
|
||||
target-depth (.nextInt rng (inc stress-max-seed-depth))]
|
||||
(new-block-safe! title)
|
||||
(recur (inc i)
|
||||
(align-depth! depth target-depth)
|
||||
(conj titles title)))
|
||||
(do
|
||||
(util/exit-edit)
|
||||
titles))))]
|
||||
(sync-by-trigger! (str "seed-" seed))
|
||||
titles))
|
||||
|
||||
(defn- next-action
|
||||
[rng]
|
||||
(nth random-edit-actions
|
||||
(.nextInt rng (count random-edit-actions))))
|
||||
|
||||
(defn- delete-existing-random-block!
|
||||
[rng known-titles]
|
||||
(loop [attempt 8]
|
||||
(if (zero? attempt)
|
||||
0
|
||||
(let [titles (vec @known-titles)]
|
||||
(if (empty? titles)
|
||||
0
|
||||
(let [title (nth titles (.nextInt rng (count titles)))
|
||||
deleted?
|
||||
(try
|
||||
(b/jump-to-block title)
|
||||
(b/delete-blocks)
|
||||
true
|
||||
(catch Throwable _
|
||||
false))]
|
||||
(if deleted?
|
||||
(do
|
||||
(swap! known-titles disj title)
|
||||
1)
|
||||
(recur (dec attempt)))))))))
|
||||
|
||||
(defn- random-edit-op!
|
||||
[rng known-titles client-prefix round op-idx]
|
||||
(let [base (format "%s-r%s-op%s" client-prefix round op-idx)]
|
||||
(case (next-action rng)
|
||||
:new
|
||||
(let [title (str base "-new")]
|
||||
(new-block-safe! title)
|
||||
(swap! known-titles conj title)
|
||||
1)
|
||||
|
||||
:save
|
||||
(let [save-title (str base "-save-updated")]
|
||||
(new-block-safe! (str base "-save"))
|
||||
(b/save-block save-title)
|
||||
(swap! known-titles conj save-title)
|
||||
2)
|
||||
|
||||
:indent-outdent
|
||||
(let [title (str base "-nest")]
|
||||
(new-block-safe! title)
|
||||
(swap! known-titles conj title)
|
||||
(+ 1
|
||||
(if (try-indent!) 1 0)
|
||||
(if (try-outdent!) 1 0)))
|
||||
|
||||
:delete-existing
|
||||
(delete-existing-random-block! rng known-titles)
|
||||
|
||||
:undo
|
||||
(do
|
||||
(b/undo)
|
||||
0)
|
||||
|
||||
:redo
|
||||
(do
|
||||
(b/redo)
|
||||
0))))
|
||||
|
||||
(defn- local-random-edit-batch!
|
||||
[rng known-titles client-prefix round]
|
||||
(let [ops (max 1 (env-int "DB_SYNC_E2E_STRESS_OPS_PER_CLIENT" stress-default-ops-per-client))]
|
||||
(loop [i 0
|
||||
undo-steps 0]
|
||||
(if (< i ops)
|
||||
(recur (inc i)
|
||||
(+ undo-steps
|
||||
(random-edit-op! rng known-titles client-prefix round i)))
|
||||
(do
|
||||
(util/exit-edit)
|
||||
undo-steps)))))
|
||||
|
||||
(defn- local-undo-redo-batch!
|
||||
[undo-steps]
|
||||
(let [steps (max 1 undo-steps)]
|
||||
;; Undo and redo exactly what this client edited in the current round.
|
||||
(b/open-last-block)
|
||||
(dotimes [_ steps]
|
||||
(b/undo))
|
||||
(dotimes [_ steps]
|
||||
(b/redo))
|
||||
(util/exit-edit)))
|
||||
|
||||
(defn- run-two-clients-without-sync!
|
||||
[p1-fn p2-fn]
|
||||
[(p1-fn)
|
||||
(p2-fn)])
|
||||
|
||||
(deftest online-two-clients-undo-redo-stress-test
|
||||
(testing "two online RTC clients survive random edits + undo/redo loops on a long nested page"
|
||||
(let [rounds (max 1 (env-int "DB_SYNC_E2E_STRESS_ROUNDS" stress-default-rounds))
|
||||
seed (long (env-int "DB_SYNC_E2E_STRESS_SEED" stress-default-seed))
|
||||
p1-rng (java.util.Random. (long (+ seed 101)))
|
||||
p2-rng (java.util.Random. (long (+ seed 202)))
|
||||
known-titles (atom (seed-long-nested-page! seed))]
|
||||
(dotimes [round rounds]
|
||||
(let [p1-undo-steps (atom 0)
|
||||
p2-undo-steps (atom 0)
|
||||
;; Phase 1: edit both online clients without forcing sync between them.
|
||||
[_ _]
|
||||
(run-two-clients-without-sync!
|
||||
#(w/with-page @*page1
|
||||
(reset! p1-undo-steps
|
||||
(local-random-edit-batch! p1-rng known-titles "p1" round)))
|
||||
#(w/with-page @*page2
|
||||
(reset! p2-undo-steps
|
||||
(local-random-edit-batch! p2-rng known-titles "p2" round))))
|
||||
p1-edit-remote-tx (w/with-page @*page1
|
||||
(-> (rtc/get-rtc-tx) :local-tx))
|
||||
p2-edit-remote-tx (w/with-page @*page2
|
||||
(-> (rtc/get-rtc-tx) :local-tx))
|
||||
;; Phase 2: undo+redo both online clients without forcing sync between them.
|
||||
[_ _]
|
||||
(run-two-clients-without-sync!
|
||||
#(w/with-page @*page1
|
||||
(local-undo-redo-batch! @p1-undo-steps))
|
||||
#(w/with-page @*page2
|
||||
(local-undo-redo-batch! @p2-undo-steps)))
|
||||
p1-undo-remote-tx (w/with-page @*page1
|
||||
(-> (rtc/get-rtc-tx) :local-tx))
|
||||
p2-undo-remote-tx (w/with-page @*page2
|
||||
(-> (rtc/get-rtc-tx) :local-tx))]
|
||||
|
||||
(sync-by-trigger!
|
||||
round
|
||||
{:p1-edit p1-edit-remote-tx
|
||||
:p2-edit p2-edit-remote-tx
|
||||
:p1-undo p1-undo-remote-tx
|
||||
:p2-undo p2-undo-remote-tx})
|
||||
(assert-two-pages-synced!)
|
||||
(assert-no-severe-sync-errors!))))))
|
||||
|
||||
;;; https://github.com/logseq/db-test/issues/651
|
||||
(deftest issue-651-block-title-double-transit-encoded-test
|
||||
(testing "
|
||||
1. create pages named \"bbb\", \"aaa\", and turn these pages into tag
|
||||
2. set \"bbb\" parent to \"aaa\"
|
||||
3. create a new page \"ccc\", and create a simple query with filter tags = aaa/bbb
|
||||
wait for 5-10 seconds, will found that \"aaa/bbb\" became \"aaa/<encrypted-string>\"
|
||||
"
|
||||
(w/with-page @*page1
|
||||
(page/new-page "aaa")
|
||||
(page/convert-to-tag "aaa")
|
||||
(page/new-page "bbb")
|
||||
(page/convert-to-tag "bbb" :extends ["aaa"])
|
||||
(page/new-page "ccc")
|
||||
(b/new-block "")
|
||||
(util/input-command "query")
|
||||
(w/click (util/-query-last "button:text('filter')"))
|
||||
(util/input "tags")
|
||||
(w/click "a.menu-link:has-text('tags')")
|
||||
(w/click "a.menu-link:has-text('bbb')")
|
||||
(util/wait-timeout 5000) ;as described in issue-url
|
||||
)
|
||||
(let [{:keys [remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(b/new-block "done")))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx)))
|
||||
|
||||
;; check the selected tag still exists after the query waits and syncs
|
||||
(w/with-page @*page1
|
||||
(page/goto-page "bbb"))
|
||||
(w/with-page @*page2
|
||||
(page/goto-page "bbb"))
|
||||
|
||||
(rtc/validate-graphs-in-2-pw-pages)))
|
||||
|
||||
(deftest paste-multiple-blocks-test
|
||||
(testing "
|
||||
1. create 3 blocks
|
||||
- block1
|
||||
- block2
|
||||
- block3
|
||||
2. copy these 3 blocks
|
||||
3. when cursor at block3, press <enter> to create a new block
|
||||
4. paste them at current position 5 times
|
||||
5. validate blocks are same on both clients"
|
||||
(w/with-page @*page1
|
||||
(b/new-blocks ["block1" "block2" "block3"])
|
||||
(util/exit-edit)
|
||||
(b/select-blocks 2)
|
||||
(b/copy)
|
||||
(b/jump-to-block "block3")
|
||||
(util/repeat-keyboard 1 "Enter"))
|
||||
|
||||
(dotimes [_ 5]
|
||||
(let [{:keys [remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(b/paste)))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx))))
|
||||
|
||||
(let [{:keys [remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(b/new-block "sync-trigger")))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx)))
|
||||
|
||||
(let [expected (vec (concat ["block1" "block2" "block3"]
|
||||
(take (* 3 5) (cycle ["block1" "block2" "block3"]))
|
||||
["sync-trigger"]))]
|
||||
(w/with-page @*page1
|
||||
(util/exit-edit)
|
||||
(is (= expected
|
||||
(util/get-page-blocks-contents))))
|
||||
|
||||
(w/with-page @*page2
|
||||
(util/exit-edit)
|
||||
(is (= expected
|
||||
(util/get-page-blocks-contents)))))
|
||||
|
||||
(rtc/validate-graphs-in-2-pw-pages)))
|
||||
|
||||
(deftest asset-blocks-validate-after-init-downloaded-test
|
||||
(testing "
|
||||
- add some assets in client1
|
||||
- remove local graph in client2
|
||||
- re-download the remote graph in client2
|
||||
- compare asset-blocks data in both clients"
|
||||
(let [asset-file "../assets/icon.png"
|
||||
page-title (w/with-page @*page1 (page/get-page-name))]
|
||||
(w/with-page @*page1
|
||||
(let [p (w/get-page)]
|
||||
(.onFileChooser p (reify java.util.function.Consumer
|
||||
(accept [_ fc]
|
||||
(.setFiles fc (into-array java.nio.file.Path [(java.nio.file.Paths/get asset-file (into-array String []))])))))
|
||||
(b/new-block "asset block")
|
||||
(util/input-command "Upload an asset")
|
||||
(w/wait-for ".ls-block img")))
|
||||
|
||||
(let [{:keys [remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(b/new-block "sync done")))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx)))
|
||||
|
||||
(w/with-page @*page2
|
||||
(graph/remove-local-graph *graph-name*)
|
||||
(graph/wait-for-remote-graph *graph-name*)
|
||||
(graph/switch-graph *graph-name* true false)
|
||||
(page/goto-page page-title)
|
||||
(w/wait-for ".ls-block img")
|
||||
(is (some? (.getAttribute (w/-query ".ls-block img") "src"))))
|
||||
|
||||
(rtc/validate-graphs-in-2-pw-pages))))
|
||||
|
||||
(deftest issue-683-paste-large-block-test
|
||||
(testing "Copying and pasting a large block of text into sync-ed graph causes sync to fail"
|
||||
(let [large-text (slurp (io/resource "large_text.txt"))]
|
||||
(w/with-page @*page1
|
||||
(w/eval-js (str "navigator.clipboard.writeText(" (json/write-value-as-string large-text) ")")))
|
||||
(let [{:keys [remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(b/new-block "")
|
||||
(b/paste)))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx)))
|
||||
|
||||
(rtc/validate-graphs-in-2-pw-pages))))
|
||||
333
clj-e2e/test/logseq/e2e/rtc_extra_test.clj
Normal file
333
clj-e2e/test/logseq/e2e/rtc_extra_test.clj
Normal file
@@ -0,0 +1,333 @@
|
||||
(ns logseq.e2e.rtc-extra-test
|
||||
(:require
|
||||
[clojure.test :refer [deftest testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.const :refer [*page1 *page2]]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.outliner-basic-test :as outliner-basic-test]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.property-basic-test :as property-basic-test]
|
||||
[logseq.e2e.rtc :as rtc :refer [validate-graphs-in-2-pw-pages]]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once
|
||||
fixtures/open-2-pages
|
||||
(partial fixtures/prepare-rtc-graph-fixture "rtc-extra-test-graph"))
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page-in-rtc)
|
||||
|
||||
(defn- with-stop-restart-rtc
|
||||
[pw-page f]
|
||||
(w/with-page pw-page
|
||||
(rtc/rtc-stop))
|
||||
(f)
|
||||
(w/with-page pw-page
|
||||
(rtc/rtc-start)))
|
||||
|
||||
(def status->icon-name
|
||||
{"Backlog" "Backlog"
|
||||
"Todo" "Todo"
|
||||
"Doing" "InProgress50"
|
||||
"In review" "InReview"
|
||||
"Done" "Done"
|
||||
"Canceled" "Cancelled"})
|
||||
|
||||
(def priorities ["No priority" "Low" "Medium" "High" "Urgent"])
|
||||
|
||||
(defn- validate-task-blocks
|
||||
[]
|
||||
(let [icon-names (vals status->icon-name)
|
||||
icon-name->count
|
||||
(w/with-page @*page2
|
||||
(into
|
||||
{}
|
||||
(map
|
||||
(fn [icon-name]
|
||||
[icon-name (.count (w/-query (str ".ls-icon-" icon-name)))])
|
||||
icon-names)))]
|
||||
(prn :validate-task-blocks icon-name->count)
|
||||
(w/with-page @*page1
|
||||
(doseq [[icon-name count*] icon-name->count]
|
||||
(assert/assert-have-count (str ".ls-icon-" icon-name) count*)))))
|
||||
|
||||
(defn- insert-task-blocks
|
||||
[title-prefix]
|
||||
(doseq [status (keys status->icon-name)
|
||||
priority priorities]
|
||||
(b/new-block (str title-prefix "-" status "-" priority))
|
||||
(util/input-command status)
|
||||
(util/input-command priority)))
|
||||
|
||||
(defn- update-task-blocks
|
||||
[]
|
||||
(let [qs-partitions (partition-all 5 (seq (.all (loc/filter ".ls-block" :has ".ui__icon"))))]
|
||||
(doseq [q-seq qs-partitions]
|
||||
(doseq [q q-seq]
|
||||
(w/click q)
|
||||
(util/input-command (rand-nth (keys status->icon-name)))
|
||||
(util/input-command (rand-nth priorities))))))
|
||||
|
||||
(deftest rtc-task-blocks-test
|
||||
(let [insert-task-blocks-in-page2
|
||||
(fn [*latest-remote-tx]
|
||||
(w/with-page @*page2
|
||||
(let [{:keys [_local-tx remote-tx]}
|
||||
(rtc/with-wait-tx-updated
|
||||
(insert-task-blocks "t1"))]
|
||||
(reset! *latest-remote-tx remote-tx))
|
||||
(util/exit-edit)))
|
||||
update-task-blocks-in-page2
|
||||
(fn [*latest-remote-tx]
|
||||
(w/with-page @*page2
|
||||
(let [{:keys [_local-tx remote-tx]}
|
||||
(rtc/with-wait-tx-updated
|
||||
(update-task-blocks))]
|
||||
(reset! *latest-remote-tx remote-tx))))]
|
||||
(testing "add some task blocks while rtc disconnected on page1"
|
||||
(let [*latest-remote-tx (atom nil)]
|
||||
(rtc/with-stop-restart-rtc
|
||||
[@*page1]
|
||||
[@*page1 (rtc/wait-tx-update-to @*latest-remote-tx)]
|
||||
(insert-task-blocks-in-page2 *latest-remote-tx))
|
||||
(validate-task-blocks)
|
||||
(validate-graphs-in-2-pw-pages)))
|
||||
|
||||
(testing "update task blocks while rtc disconnected on page1"
|
||||
(let [*latest-remote-tx (atom nil)]
|
||||
(rtc/with-stop-restart-rtc
|
||||
[@*page1]
|
||||
[@*page1 (rtc/wait-tx-update-to @*latest-remote-tx)]
|
||||
(update-task-blocks-in-page2 *latest-remote-tx))
|
||||
(validate-task-blocks)
|
||||
(validate-graphs-in-2-pw-pages)))
|
||||
|
||||
(fixtures/new-logseq-page-in-rtc*)
|
||||
|
||||
(testing "perform same operations on page2 while keeping rtc connected on page1"
|
||||
(let [*latest-remote-tx (atom nil)]
|
||||
(insert-task-blocks-in-page2 *latest-remote-tx)
|
||||
(w/with-page @*page1
|
||||
(rtc/wait-tx-update-to @*latest-remote-tx))
|
||||
(validate-task-blocks)
|
||||
(validate-graphs-in-2-pw-pages)))
|
||||
|
||||
(testing "update task blocks while rtc connected on page1"
|
||||
(let [*latest-remote-tx (atom nil)]
|
||||
(update-task-blocks-in-page2 *latest-remote-tx)
|
||||
(w/with-page @*page1
|
||||
(rtc/wait-tx-update-to @*latest-remote-tx))
|
||||
(validate-task-blocks)
|
||||
(validate-graphs-in-2-pw-pages)))))
|
||||
|
||||
(deftest rtc-property-test
|
||||
(let [insert-new-property-blocks-in-page2
|
||||
(fn [*latest-remote-tx title-prefix]
|
||||
(w/with-page @*page2
|
||||
(let [{:keys [_local-tx remote-tx]}
|
||||
(rtc/with-wait-tx-updated
|
||||
(property-basic-test/add-new-properties title-prefix))]
|
||||
(reset! *latest-remote-tx remote-tx))))]
|
||||
(testing "add different types user properties on page2 while keeping rtc connected on page1"
|
||||
(let [*latest-remote-tx (atom nil)]
|
||||
(rtc/with-stop-restart-rtc
|
||||
[@*page1]
|
||||
[@*page1 (rtc/wait-tx-update-to @*latest-remote-tx)]
|
||||
(insert-new-property-blocks-in-page2 *latest-remote-tx "rtc-property-test-1"))
|
||||
(validate-graphs-in-2-pw-pages)))
|
||||
|
||||
(fixtures/new-logseq-page-in-rtc*)
|
||||
|
||||
(testing "perform same operations on page2 while keeping rtc connected on page1"
|
||||
(let [*latest-remote-tx (atom nil)]
|
||||
(insert-new-property-blocks-in-page2 *latest-remote-tx "rtc-property-test-2")
|
||||
(w/with-page @*page1
|
||||
(rtc/wait-tx-update-to @*latest-remote-tx))
|
||||
(validate-graphs-in-2-pw-pages)))))
|
||||
|
||||
(deftest rtc-property-update-rerenders-mounted-block
|
||||
(testing "a remote property transaction rerenders the mounted block"
|
||||
(let [title "rtc live property block"
|
||||
property-name "rtc-live-property"
|
||||
property-value "rtc remote property value"
|
||||
*block-uuid (atom nil)
|
||||
{:keys [remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(let [block (ls-api-call! :editor.appendBlockInPage title)]
|
||||
(reset! *block-uuid (get block "uuid")))))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx)
|
||||
(w/wait-for
|
||||
(format "#ls-block-%s .block-title-wrap:text('%s')"
|
||||
@*block-uuid title)))
|
||||
|
||||
(let [{:keys [remote-tx]}
|
||||
(w/with-page @*page2
|
||||
(rtc/with-wait-tx-updated
|
||||
(ls-api-call! :editor.upsertBlockProperty
|
||||
@*block-uuid property-name property-value)))]
|
||||
(w/with-page @*page1
|
||||
(rtc/wait-tx-update-to remote-tx)
|
||||
(w/wait-for
|
||||
(format "#ls-block-%s .property-k:text('%s')"
|
||||
@*block-uuid property-name))
|
||||
(assert/assert-is-visible
|
||||
(format "#ls-block-%s .property-value :text('%s')"
|
||||
@*block-uuid property-value))))
|
||||
(validate-graphs-in-2-pw-pages))))
|
||||
|
||||
(deftest rtc-outliner-test
|
||||
(doseq [test-fn [outliner-basic-test/create-test-page-and-insert-blocks
|
||||
outliner-basic-test/indent-and-outdent
|
||||
outliner-basic-test/move-up-down
|
||||
outliner-basic-test/delete
|
||||
outliner-basic-test/delete-test-with-children]]
|
||||
(let [test-fn-in-page2 (fn [*latest-remote-tx]
|
||||
(w/with-page @*page2
|
||||
(let [{:keys [_local-tx remote-tx]}
|
||||
(rtc/with-wait-tx-updated
|
||||
(test-fn))]
|
||||
(reset! *latest-remote-tx remote-tx))))
|
||||
*latest-remote-tx (atom nil)]
|
||||
(fixtures/new-logseq-page-in-rtc*)
|
||||
(rtc/with-stop-restart-rtc
|
||||
[@*page1]
|
||||
[@*page1 (rtc/wait-tx-update-to @*latest-remote-tx)]
|
||||
(test-fn-in-page2 *latest-remote-tx))
|
||||
(validate-graphs-in-2-pw-pages))))
|
||||
|
||||
(deftest rtc-outliner-conflict-update-test
|
||||
(let [title-prefix "rtc-outliner-conflict-update-test"]
|
||||
(testing "add some blocks, ensure them synced"
|
||||
(let [*latest-remote-tx (atom nil)]
|
||||
(w/with-page @*page1
|
||||
(let [{:keys [_local-tx remote-tx]}
|
||||
(rtc/with-wait-tx-updated
|
||||
(b/new-blocks (map #(str title-prefix "-" %) (range 10))))]
|
||||
(reset! *latest-remote-tx remote-tx)))
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to @*latest-remote-tx))
|
||||
(validate-graphs-in-2-pw-pages)))
|
||||
(testing "page1: indent block1 as child of block0, page2: delete block0"
|
||||
(rtc/with-stop-restart-rtc
|
||||
[@*page1 @*page2]
|
||||
[@*page1 (rtc/with-wait-tx-updated
|
||||
(k/esc)
|
||||
(assert/assert-in-normal-mode?)
|
||||
(b/new-block "page1-done-1"))
|
||||
@*page2 (rtc/with-wait-tx-updated
|
||||
(k/esc)
|
||||
(assert/assert-in-normal-mode?)
|
||||
(b/new-block "page2-done-1"))]
|
||||
(w/with-page @*page1
|
||||
(w/click (format ".ls-block :text('%s')" (str title-prefix "-" 1)))
|
||||
(b/indent))
|
||||
(w/with-page @*page2
|
||||
(w/click (format ".ls-block :text('%s')" (str title-prefix "-" 0)))
|
||||
(b/delete-blocks)))
|
||||
(validate-graphs-in-2-pw-pages))
|
||||
(testing "
|
||||
origin:
|
||||
- block2
|
||||
- block3
|
||||
- block4
|
||||
page1:
|
||||
- block2
|
||||
- block3
|
||||
- block4
|
||||
page2:
|
||||
;; block2 deleted
|
||||
- block4
|
||||
- block3"
|
||||
(rtc/with-stop-restart-rtc
|
||||
[@*page1 @*page2]
|
||||
[@*page1 (rtc/with-wait-tx-updated (b/new-block "page1-done-2"))
|
||||
@*page2 (rtc/with-wait-tx-updated (b/new-block "page2-done-2"))]
|
||||
(w/with-page @*page1
|
||||
(w/click (format ".ls-block :text('%s')" (str title-prefix "-" 3)))
|
||||
(b/indent)
|
||||
(k/arrow-down)
|
||||
(b/indent)
|
||||
(b/indent))
|
||||
(w/with-page @*page2
|
||||
(w/click (format ".ls-block :text('%s')" (str title-prefix "-" 2)))
|
||||
(b/delete-blocks)
|
||||
(w/click (format ".ls-block :text('%s')" (str title-prefix "-" 3)))
|
||||
(k/shift+arrow-down)
|
||||
(k/meta+shift+arrow-down)
|
||||
(k/enter)
|
||||
(b/indent)))
|
||||
(validate-graphs-in-2-pw-pages))))
|
||||
|
||||
(deftest rtc-page-test
|
||||
(let [prefix "rtc-page-test-"]
|
||||
(testing "create same name page in different clients.
|
||||
- client1: offline, create page1
|
||||
- client2: offline, create page1
|
||||
- restart rtc"
|
||||
(rtc/with-stop-restart-rtc
|
||||
[@*page1 @*page2]
|
||||
[@*page1 (rtc/with-wait-tx-updated (b/new-block "pw1-done-1"))
|
||||
@*page2 (rtc/with-wait-tx-updated (b/new-block "pw2-done-1"))]
|
||||
(w/with-page @*page1
|
||||
(page/new-page (str prefix 1)))
|
||||
(w/with-page @*page2
|
||||
(page/new-page (str prefix 1))))
|
||||
(validate-graphs-in-2-pw-pages))
|
||||
|
||||
(testing "
|
||||
- client1: offline, add blocks on page-2
|
||||
- client2: offline, delete page-2"
|
||||
(let [page-name (str prefix 2)]
|
||||
(let [*latest-remote-tx (atom nil)]
|
||||
(w/with-page @*page1
|
||||
(let [{:keys [_local-tx remote-tx]}
|
||||
(rtc/with-wait-tx-updated
|
||||
(page/new-page page-name))]
|
||||
(reset! *latest-remote-tx remote-tx)))
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to @*latest-remote-tx)))
|
||||
(validate-graphs-in-2-pw-pages)
|
||||
|
||||
(rtc/with-stop-restart-rtc
|
||||
[@*page1 @*page2]
|
||||
[@*page1 (rtc/with-wait-tx-updated (b/new-block "pw1-done-2"))
|
||||
@*page2 (rtc/with-wait-tx-updated (b/new-block "pw2-done-2"))]
|
||||
(w/with-page @*page1
|
||||
(b/new-blocks (map #(str "block-" %) (range 5))))
|
||||
(w/with-page @*page2
|
||||
(page/delete-page page-name)))
|
||||
(validate-graphs-in-2-pw-pages)))
|
||||
(testing "page rename"
|
||||
(let [page-name (str prefix 3)]
|
||||
(fixtures/new-logseq-page-in-rtc* page-name)
|
||||
(validate-graphs-in-2-pw-pages)
|
||||
(rtc/with-stop-restart-rtc
|
||||
[@*page1 @*page2]
|
||||
[@*page1 (rtc/with-wait-tx-updated (b/new-block "pw1-done-3"))
|
||||
@*page2 (rtc/with-wait-tx-updated (b/new-block "pw2-done-3"))]
|
||||
(w/with-page @*page1
|
||||
(page/rename-page page-name (str page-name "-rename1")))
|
||||
(w/with-page @*page2
|
||||
(page/rename-page page-name (str page-name "-rename2"))))
|
||||
(validate-graphs-in-2-pw-pages)))))
|
||||
|
||||
(deftest long-block-title-test
|
||||
(testing "insert >4KB block/title"
|
||||
(let [long-block-title (apply str (repeat 5000 "a"))
|
||||
{:keys [_local-tx remote-tx]}
|
||||
(w/with-page @*page1
|
||||
(rtc/with-wait-tx-updated
|
||||
(b/new-block "")
|
||||
(b/save-block long-block-title)))]
|
||||
(w/with-page @*page2
|
||||
(rtc/wait-tx-update-to remote-tx))
|
||||
(validate-graphs-in-2-pw-pages)
|
||||
(w/with-page @*page2
|
||||
(assert/assert-is-visible (loc/filter ".block-title-wrap" :has-text long-block-title))))))
|
||||
57
clj-e2e/test/logseq/e2e/tag_basic_test.clj
Normal file
57
clj-e2e/test/logseq/e2e/tag_basic_test.clj
Normal file
@@ -0,0 +1,57 @@
|
||||
(ns logseq.e2e.tag-basic-test
|
||||
(:require [clojure.test :refer [deftest use-fixtures]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(defn add-new-tags
|
||||
[title-prefix]
|
||||
(b/new-block (str title-prefix 1 " #" title-prefix "1"))
|
||||
(util/double-esc)
|
||||
(b/new-block (str title-prefix 2))
|
||||
(util/set-tag (str title-prefix 2)))
|
||||
|
||||
(deftest new-tag-test
|
||||
(add-new-tags "tag-test-"))
|
||||
|
||||
(deftest page-tag-conversion-persists-and-removes-tag-from-objects-test
|
||||
(let [tag-name "page-tag-conversion"
|
||||
object-page "page-tag-object"]
|
||||
(page/new-page tag-name)
|
||||
(k/esc)
|
||||
(page/convert-to-tag tag-name)
|
||||
(assert/assert-is-visible "div[data-testid='page title'] :text('Tag')")
|
||||
|
||||
(page/new-page object-page)
|
||||
(b/save-block "Tagged object")
|
||||
(util/set-tag tag-name)
|
||||
(k/esc)
|
||||
|
||||
(page/goto-page tag-name)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body" :has-text "Tagged object"))
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body" :has-text "Tagged object"))
|
||||
|
||||
(w/click ".toolbar-dots-btn")
|
||||
(w/click (loc/filter "[role='menuitem']" :has-text "Convert Tag to Page"))
|
||||
(w/click "div[role='alertdialog'] button:text('Confirm')")
|
||||
(assert/assert-have-count "button:text('Add tag property')" 0)
|
||||
(assert/assert-have-count ".ls-view-body" 0)
|
||||
|
||||
(page/goto-page object-page)
|
||||
(assert/assert-have-count
|
||||
(format ".block-tag :text('%s')" tag-name)
|
||||
0)))
|
||||
94
clj-e2e/test/logseq/e2e/undo_redo_test.clj
Normal file
94
clj-e2e/test/logseq/e2e/undo_redo_test.clj
Normal file
@@ -0,0 +1,94 @@
|
||||
(ns logseq.e2e.undo-redo-test
|
||||
(:require
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as string]
|
||||
[clojure.test :refer [deftest testing is use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.page :as p]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]
|
||||
[wally.repl :as repl]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(deftest undo-redo-paste
|
||||
(testing "Undo redo paste blocks"
|
||||
(b/new-blocks ["b1" "b2"])
|
||||
(b/select-blocks 2)
|
||||
(b/copy)
|
||||
(b/new-block "")
|
||||
(b/paste)
|
||||
(util/exit-edit)
|
||||
(is (= ["b1" "b2" "b1" "b2"] (util/get-page-blocks-contents)))
|
||||
(b/undo)
|
||||
(util/exit-edit)
|
||||
(is (= ["b1" "b2" ""] (util/get-page-blocks-contents)))
|
||||
(b/redo)
|
||||
(util/exit-edit)
|
||||
(is (= ["b1" "b2" "b1" "b2"] (util/get-page-blocks-contents)))))
|
||||
|
||||
(deftest undo-latest-saved-block-content-once
|
||||
(testing "Undo reverts the latest saved block content on the first attempt"
|
||||
(b/new-blocks ["b1"])
|
||||
(util/wait-timeout 2000)
|
||||
(util/move-cursor-to-end)
|
||||
(util/press-seq " new text" :delay 20)
|
||||
(util/wait-timeout 1000)
|
||||
(b/undo)
|
||||
(util/exit-edit)
|
||||
(is (= ["b1"] (util/get-page-blocks-contents)))))
|
||||
|
||||
(deftest cut-and-paste-preserves-multiple-block-trees
|
||||
(testing "Cut waits for structured clipboard data before deleting nested blocks"
|
||||
(let [page-name (p/get-page-name)
|
||||
page-uuid (get (ls-api-call! :editor.getBlock page-name) "uuid")
|
||||
target-uuid (get (first (ls-api-call! :editor.getPageBlocksTree page-name))
|
||||
"uuid")]
|
||||
(util/exit-edit)
|
||||
(ls-api-call! :editor.insertBatchBlock
|
||||
page-uuid
|
||||
[{:content "parent a"
|
||||
:children [{:content "child a1"}
|
||||
{:content "child a2"}]}
|
||||
{:content "parent b"
|
||||
:children [{:content "child b1"}
|
||||
{:content "child b2"}]}])
|
||||
(w/click (util/get-by-text "child b2" true))
|
||||
(b/select-blocks 6)
|
||||
(k/press "ControlOrMeta+x" {:delay 100})
|
||||
(loop [remaining 50]
|
||||
(let [blocks (ls-api-call! :editor.getPageBlocksTree page-name)]
|
||||
(when (some #(not (string/blank? (get % "content"))) blocks)
|
||||
(when (zero? remaining)
|
||||
(throw (ex-info "Cut did not remove the selected block trees" {})))
|
||||
(util/wait-timeout 100)
|
||||
(recur (dec remaining)))))
|
||||
(w/click (format "#ls-block-%s .block-content" target-uuid))
|
||||
(b/paste)
|
||||
(util/exit-edit)
|
||||
(let [tree (loop [remaining 50]
|
||||
(let [blocks (remove #(string/blank? (get % "content"))
|
||||
(ls-api-call! :editor.getPageBlocksTree page-name))]
|
||||
(if (or (= 2 (count blocks)) (zero? remaining))
|
||||
blocks
|
||||
(do
|
||||
(util/wait-timeout 100)
|
||||
(recur (dec remaining))))))
|
||||
titles (fn [blocks]
|
||||
(mapv #(get % "content") blocks))
|
||||
children (fn [block]
|
||||
(titles (get block "children")))]
|
||||
(is (= ["parent a" "parent b"] (titles tree)))
|
||||
(is (= ["child a1" "child a2"] (children (first tree))))
|
||||
(is (= ["child b1" "child b2"] (children (second tree)))))
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-is-visible (util/get-by-text "parent a" true))
|
||||
(assert/assert-is-visible (util/get-by-text "parent b" true)))))
|
||||
229
clj-e2e/test/logseq/e2e/view_basic_test.clj
Normal file
229
clj-e2e/test/logseq/e2e/view_basic_test.clj
Normal file
@@ -0,0 +1,229 @@
|
||||
(ns logseq.e2e.view-basic-test
|
||||
(:require [clojure.string :as string]
|
||||
[clojure.test :refer [deftest is use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.keyboard :as k]
|
||||
[logseq.e2e.locator :as loc]
|
||||
[logseq.e2e.page :as page]
|
||||
[logseq.e2e.util :as util]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(defn- select-view-type
|
||||
[view-type]
|
||||
(w/click ".view-action-type")
|
||||
(w/click (util/get-by-text view-type true)))
|
||||
|
||||
(defn- seed-table-view!
|
||||
[tag-name]
|
||||
(let [container-page (page/get-page-name)]
|
||||
(ls-api-call! :editor.createTag
|
||||
tag-name
|
||||
{:tagProperties [{:name "Status"}
|
||||
{:name "Priority"
|
||||
:schema {:type "number"}}]})
|
||||
(doseq [[title status priority]
|
||||
[["Alpha table object" "Open" 2]
|
||||
["Beta table object" "Closed" 1]]]
|
||||
(ls-api-call! :editor.insertBlock
|
||||
container-page
|
||||
(str title " #" tag-name)
|
||||
{:properties {"Status" status
|
||||
"Priority" priority}}))
|
||||
(page/goto-page tag-name)
|
||||
(assert/assert-is-visible ".ls-view-body .ls-table-header-cell")
|
||||
(w/click ".views button[title='Add new view']")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".views > button" :has-text "New view"))
|
||||
(select-view-type "List View")
|
||||
(assert/assert-is-visible ".view-action-type .ls-icon-list")
|
||||
(select-view-type "Table View")
|
||||
(assert/assert-is-visible ".view-action-type .ls-icon-table")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body .ls-table-row" :has-text "Alpha table object"))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body .ls-table-row" :has-text "Beta table object"))))
|
||||
|
||||
(defn- view-action-button
|
||||
[icon]
|
||||
(.first (.locator (w/get-page)
|
||||
(str ".view-actions button:has(.ls-icon-" icon ")"))))
|
||||
|
||||
(defn- open-view-more-actions!
|
||||
[]
|
||||
(w/click (view-action-button "dots")))
|
||||
|
||||
(defn- open-view-submenu!
|
||||
[label]
|
||||
(let [item-index ({"Columns visibility" 0
|
||||
"Group by" 1
|
||||
"Sort groups by" 2
|
||||
"Sort groups order" 3}
|
||||
label)]
|
||||
(k/press "Home")
|
||||
(dotimes [_ item-index]
|
||||
(k/arrow-down))
|
||||
(k/arrow-right)))
|
||||
|
||||
(deftest table-row-selection-shows-action-bar-test
|
||||
(seed-table-view! "table-row-selection-actions")
|
||||
(doseq [title ["Alpha table object" "Beta table object"]]
|
||||
(let [row (loc/filter ".ls-view-body .ls-table-row" :has-text title)]
|
||||
(w/click (.locator row "[data-table-row-select]"))))
|
||||
(assert/assert-is-visible ".ls-table-actions")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-table-actions .selection-count" :has-text "2")))
|
||||
|
||||
(deftest view-lifecycle-and-display-type-persistence-test
|
||||
(let [tag-name "view-lifecycle"
|
||||
container-page (page/get-page-name)
|
||||
object-title "view lifecycle object"
|
||||
view-title "Persistent view"]
|
||||
(ls-api-call! :editor.createTag tag-name)
|
||||
(ls-api-call! :editor.insertBlock
|
||||
container-page
|
||||
(str object-title " #" tag-name))
|
||||
(page/goto-page tag-name)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body" :has-text object-title))
|
||||
|
||||
(w/click ".views button[title='Add new view']")
|
||||
(w/click (loc/filter ".views > button" :has-text "New view"))
|
||||
(w/click (loc/filter "[role='menuitem']" :has-text "Rename"))
|
||||
(w/click "[role='menu'] .block-title-wrap")
|
||||
(util/press-seq view-title)
|
||||
(k/enter)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".views > button" :has-text view-title))
|
||||
(util/double-esc)
|
||||
|
||||
(select-view-type "List View")
|
||||
(assert/assert-is-visible ".view-action-type .ls-icon-list")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body .ls-block" :has-text object-title))
|
||||
|
||||
(select-view-type "Gallery View")
|
||||
(assert/assert-is-visible ".view-action-type .ls-icon-layout-grid")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-card-item" :has-text object-title))
|
||||
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".views > button" :has-text view-title))
|
||||
(assert/assert-is-visible ".view-action-type .ls-icon-table")
|
||||
(w/click (loc/filter ".views > button" :has-text view-title))
|
||||
(assert/assert-is-visible ".view-action-type .ls-icon-layout-grid")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-card-item" :has-text object-title))
|
||||
|
||||
(w/click (loc/filter ".views > button" :has-text view-title))
|
||||
(w/click (loc/filter "[role='menuitem']" :has-text "Delete"))
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".views > button" :has-text view-title)
|
||||
0)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".views > button" :has-text "All"))))
|
||||
|
||||
(deftest table-view-search-filter-and-new-record-actions-test
|
||||
(seed-table-view! "table-search-filter-actions")
|
||||
|
||||
(w/click (view-action-button "search"))
|
||||
(w/fill "input[placeholder='Type to search']" "Alpha")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body .ls-table-row" :has-text "Alpha table object"))
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".ls-view-body .ls-table-row" :has-text "Beta table object")
|
||||
0)
|
||||
(w/click (view-action-button "search"))
|
||||
(w/fill "input[placeholder='Type to search']" "")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body .ls-table-row" :has-text "Beta table object"))
|
||||
|
||||
(w/click (view-action-button "filter"))
|
||||
(w/click (loc/filter ".cp__select-results a" :has-text "Status"))
|
||||
(assert/assert-is-visible ".cp__select-input[placeholder='Status']")
|
||||
(w/click (util/get-by-text "Is Not Empty" true))
|
||||
(assert/assert-is-visible ".filters-row")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body .ls-table-row" :has-text "Alpha table object"))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body .ls-table-row" :has-text "Beta table object"))
|
||||
(util/double-esc)
|
||||
(w/click ".filters-row button:has(.ls-icon-x)")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body .ls-table-row" :has-text "Alpha table object"))
|
||||
|
||||
(w/click (view-action-button "plus"))
|
||||
(assert/assert-is-visible ".cp__right-sidebar.open")
|
||||
(util/wait-editor-visible)
|
||||
(util/press-seq "New table object")
|
||||
(util/exit-edit)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body .ls-table-row" :has-text "New table object")))
|
||||
|
||||
(deftest table-view-column-visibility-action-test
|
||||
(seed-table-view! "table-column-actions")
|
||||
(open-view-more-actions!)
|
||||
(open-view-submenu! "Columns visibility")
|
||||
(w/click (loc/filter "[role='menuitemcheckbox']" :has-text "Status"))
|
||||
(assert/assert-have-count
|
||||
(loc/filter ".ls-table-header-cell" :has-text "Status")
|
||||
0)
|
||||
(util/double-esc)
|
||||
|
||||
(open-view-more-actions!)
|
||||
(open-view-submenu! "Columns visibility")
|
||||
(w/click (loc/filter "[role='menuitemcheckbox']" :has-text "Status"))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-table-header-cell" :has-text "Status"))
|
||||
(util/double-esc)
|
||||
|
||||
(util/refresh-until-graph-loaded)
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-table-header-cell" :has-text "Status")))
|
||||
|
||||
(deftest table-view-group-and-export-actions-test
|
||||
(seed-table-view! "table-group-export-actions")
|
||||
|
||||
(open-view-more-actions!)
|
||||
(open-view-submenu! "Group by")
|
||||
(w/click (loc/filter "[role='menuitemcheckbox']" :has-text "Status"))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body" :has-text "Open"))
|
||||
(assert/assert-is-visible
|
||||
(loc/filter ".ls-view-body" :has-text "Closed"))
|
||||
(util/double-esc)
|
||||
|
||||
(open-view-more-actions!)
|
||||
(open-view-submenu! "Sort groups by")
|
||||
(w/click (loc/filter "[role='menuitemcheckbox']" :has-text "Page name"))
|
||||
(util/double-esc)
|
||||
(open-view-more-actions!)
|
||||
(open-view-submenu! "Sort groups by")
|
||||
(assert/assert-is-visible
|
||||
(loc/filter "[role='menuitemcheckbox'][aria-checked='true']"
|
||||
:has-text "Page name"))
|
||||
(util/double-esc)
|
||||
|
||||
(open-view-more-actions!)
|
||||
(open-view-submenu! "Sort groups order")
|
||||
(w/click (loc/filter "[role='menuitemcheckbox']" :has-text "Descending"))
|
||||
(util/double-esc)
|
||||
(let [body-text (.innerText (.locator (w/get-page) ".ls-view-body"))]
|
||||
(is (< (string/index-of body-text "Open")
|
||||
(string/index-of body-text "Closed"))))
|
||||
|
||||
(open-view-more-actions!)
|
||||
(w/click (.last (loc/filter "[role='menuitem']" :has-text "Export EDN")))
|
||||
(assert/assert-is-visible
|
||||
(util/get-by-text "Copied view nodes" false))
|
||||
(let [content (w/eval-js "navigator.clipboard.readText()")]
|
||||
(is (string/includes? content "Alpha table object"))
|
||||
(is (string/includes? content "Beta table object"))))
|
||||
Reference in New Issue
Block a user