diff --git a/dev-src/braid/dev/figwheel.clj b/dev-src/braid/dev/figwheel.clj index e7441774e..5e8b3c771 100644 --- a/dev-src/braid/dev/figwheel.clj +++ b/dev-src/braid/dev/figwheel.clj @@ -7,7 +7,6 @@ :start (repl-api/start {:mode :serve :open-url false - :ring-server-options {:port 3559} :connect-url "ws://[[client-hostname]]:[[server-port]]/figwheel-connect" :watch-dirs ["src"]} "dev") diff --git a/project.clj b/project.clj index e06099671..a047bc1cb 100644 --- a/project.clj +++ b/project.clj @@ -49,7 +49,10 @@ [refactor-nrepl "2.4.0"]]}] :test - [:dev] + [:dev + ;; etaoin requires: sudo apt-get install chromium-chromedriver + ;; on mac: brew cask install chromedriver OR brew install --cask chromedriver + {:dependencies [[etaoin "0.4.0"]]}] :uberjar [:prod diff --git a/src/braid/base/client/socket.cljs b/src/braid/base/client/socket.cljs index b87a01fb0..3a42c30ba 100644 --- a/src/braid/base/client/socket.cljs +++ b/src/braid/base/client/socket.cljs @@ -108,12 +108,18 @@ (sente/chsk-reconnect! chsk)) (defn connect! [] - (if-not chsk + (if (or (not chsk) + (not (:csrf-token @chsk-state))) (xhr/edn-xhr {:uri "/csrf" :method :get :on-complete (fn [resp] - (make-socket! (:token resp)) - (start-router!) - (start-ping-loop))}) + (if chsk + (do + (swap! chsk-state assoc :csrf-token (resp :token)) + (reconnect!)) + (do + (make-socket! (:token resp)) + (start-router!) + (start-ping-loop))))}) (reconnect!))) diff --git a/src/braid/chat/client/events.cljs b/src/braid/chat/client/events.cljs index 12783ad46..d0788633f 100644 --- a/src/braid/chat/client/events.cljs +++ b/src/braid/chat/client/events.cljs @@ -405,6 +405,7 @@ :headers {"x-csrf-token" (:csrf-token @socket/chsk-state)} :on-complete (fn [data] (socket/disconnect!) + (swap! socket/chsk-state assoc :csrf-token nil) (dispatch [:initialize-db!]) (dispatch [:set-login-state! :gateway]) (dispatch [:go-to! "/"]))}})) diff --git a/test/braid/test/lib/xpath.clj b/test/braid/test/lib/xpath.clj new file mode 100644 index 000000000..798318dfc --- /dev/null +++ b/test/braid/test/lib/xpath.clj @@ -0,0 +1,41 @@ +(ns braid.test.lib.xpath + (:require + [clojure.string :as string] + [instaparse.core :as insta])) + +(def css-selector-parser + (insta/parser + "S ::= ( CLASS-SELECTOR | COMBINATOR ) * + + CLASS-SELECTOR ::= '.' CLASS + CLASS ::= #'[a-zA-Z-]+' + COMBINATOR ::= DESCENDANT-COMB | DIRECT-CHILD-COMB + DESCENDANT-COMB ::= ' '+ + DIRECT-CHILD-COMB ::= '>' | ' > ' + + ")) + +(defn css->xpath + [css-selector] + (->> (css-selector-parser css-selector) + (insta/transform {:CLASS-SELECTOR (fn [_ [_ class-name]] + class-name) + :COMBINATOR (fn [[type]] + (case type + :DIRECT-CHILD-COMB "/" + :DESCENDANT-COMB "//"))}) + rest + (map (fn [arg] + (cond + (#{"/" "//"} arg) + arg + + (string? arg) + (str "*[contains(concat(' ',normalize-space(@class),' '),' " + arg + " ')]")))) + string/join + (str "//"))) + +(comment + (prn (css->xpath ".foo .bar"))) diff --git a/test/e2e/braid/e2e.clj b/test/e2e/braid/e2e.clj new file mode 100644 index 000000000..7f07f1d2e --- /dev/null +++ b/test/e2e/braid/e2e.clj @@ -0,0 +1,228 @@ +(ns e2e.braid.e2e + (:require + [clojure.java.io :as io] + [clojure.test :refer [deftest is testing use-fixtures]] + [datomic.api] + [etaoin.xpath] + [etaoin.api :as e] + [etaoin.dev :as e.devtools] + [etaoin.keys :as e.keys] + [braid.base.conf :as conf] + [braid.dev.core :as dev.core] + [braid.test.lib.xpath :refer [css->xpath]] + [braid.search.lucene :as lucene])) + +(defn e2e-fixture + [t] + (when (map? conf/config) + (dev.core/drop-db!) + (reset! lucene/-store nil) + (dev.core/stop!)) + (dev.core/start! 5551) + (dev.core/seed!) + (dev.core/disable-request-logging!) + (t)) + +(use-fixtures :once e2e-fixture) + +(defonce driver (e/chrome-headless {:dev {:perf {:level :all + :network? true}}})) + +(defmethod etaoin.xpath/clause :fn/has-string + [[_ text]] + (etaoin.xpath/node-contains "string()" text)) + +(defn check-for [driver query] + (e/wait-visible driver query {:timeout 2})) + +(defn check-not [driver query] + (e/wait-absent driver query {:timeout 2})) + +(defn xpath-contains-string + [css-selector string] + (->> string + (format "[contains(. , \"%s\")]") + (str (css->xpath css-selector)))) + +(defn check-requests [driver] + (is (empty? (->> driver + e.devtools/get-requests + (filter e.devtools/request-failed?))))) + +(defonce keyboard (e/make-key-input)) + +(defn press-key + [driver key] + (->> (e/add-key-press keyboard key) + (e/perform-actions driver))) + +(deftest ^:test-refresh/focus e2e + (let [d driver] + (.mkdirs (io/file "target/test-postmortems")) + (e/with-postmortem d + {:dir "target/test-postmortems"} + + (testing "Sending messages" + (e/go d "http://localhost:5551") + (check-requests d) + + (testing "Log in as @foo" + (check-for d {:tag :h1 :fn/has-string "Log in to Braid"}) + (e/fill d {:tag :input :type "email"} "foo@example.com") + (e/fill d {:tag :input :type "password"} "foofoofoo") + (e/click d {:tag :button :type "submit" :fn/has-string "Log in to Braid"}) + (check-requests d)) + + (testing "Switch group" + (e/click-visible d {:tag :a :fn/has-class "group" :title "Braid"}) + (check-requests d)) + + (testing "View homepage" + (check-for d {:xpath (xpath-contains-string ".group-header .group-name" + "Braid")}) + (check-for d {:xpath (xpath-contains-string ".user-header .name" + "@foo")}) + (check-for d {:xpath (xpath-contains-string ".card .tag .name" + "#braid")}) + (check-for d {:xpath (xpath-contains-string ".message .content" + "Hello?")}) + (check-for d {:xpath (xpath-contains-string ".message .content" + "Yep!")})) + + (testing "Send messages in existing thread" + (e/fill d [{:class "textarea"} {:tag :textarea}] + "Yo @bar buddy!") + (press-key d e.keys/enter) + (check-for d {:xpath (xpath-contains-string ".message .content" + "buddy!")}) + (check-for d {:xpath (xpath-contains-string ".content .dummy .user .name" + "@bar")}) + (e/fill d [{:class "textarea"} {:tag :textarea}] + "Long time no talk!!") + (press-key d e.keys/enter) + (check-for d {:xpath (xpath-contains-string ".message .content" + "Long time no talk!!")}) + (check-requests d)) + + (testing "Send messages in a new thread" + (e/click d {:tag :button :class "new-thread"}) + (e/fill-human d {:tag :textarea :index 1} "Welcome to #new-thread") + (e/wait d 1) ; for tag creation to work + (press-key d e.keys/enter) + (press-key d e.keys/enter) + (check-for d {:xpath (xpath-contains-string ".message .content" + "Welcome to #new-thread")}) + (e/fill-human d {:tag :textarea :index 1} "This thread is gonna be awesome!") + (press-key d e.keys/enter) + (check-for d {:xpath (xpath-contains-string ".message .content" + "This thread is gonna be awesome!")}) + (press-key d e.keys/enter) + (check-requests d)) + + (testing "Log out" + (e/mouse-move-to d {:tag :div :class "more"}) + (e/click d {:tag :a :fn/has-string "Log Out"}) + (check-for d {:tag :h1 :fn/has-string "Log in to Braid"}) + (check-requests d))) + + (testing "Receiving messages" + (testing "Log in as @bar" + (e/fill d {:tag :input :type "email"} "bar@example.com") + (e/fill d {:tag :input :type "password"} "barbarbar") + (e/click d {:tag :button :type "submit" :fn/has-string "Log in to Braid"}) + (check-for d {:xpath (xpath-contains-string ".user-header .name" + "@bar")}) + (check-requests d)) + + (testing "Switch group" + (e/click-visible d {:tag :a :fn/has-class "group" :title "Braid"}) + (check-requests d)) + + (testing "View received messages" + (check-for d {:xpath (xpath-contains-string ".message .content" + "Long time no talk!")}) + (check-for d {:xpath (xpath-contains-string ".content .dummy .user .name" + "@bar")}) + (check-for d {:xpath (xpath-contains-string ".message .content" + "Welcome to #new-thread")}) + (check-for d {:xpath (xpath-contains-string ".message .content" + "This thread is gonna be awesome!")}))) + + (testing "Closing and bringing back threads" + (testing "Close a thread" + (e/click d {:xpath (css->xpath ".close")}) + (check-not d {:xpath (xpath-contains-string ".message .content" + "Welcome to #new-thread")}) + (check-requests d)) + + (testing "Bring back the closed thread from Recent Page" + (e/click d {:tag :a :title "Recently Closed"}) + (check-for d {:xpath (xpath-contains-string ".message .content" + "Welcome to #new-thread")}) + (e/click d {:tag :div :title "Mark Unread"}) + (check-not d {:xpath (css->xpath ".messages")}) + (check-requests d)) + + (testing "See the thread back on Inbox Page" + (e/click d {:tag :a :title "Inbox"}) + (check-for d {:xpath (xpath-contains-string ".message .content" + "Welcome to #new-thread")}) + (check-requests d))) + + (testing "Starring threads" + (testing "Star a thread" + (e/click d {:tag :div :class "star not-starred"}) + (check-for d {:tag :div :class "star starred"}) + (check-requests d)) + + (testing "See starred thread on Starred Threads Page" + (e/click d {:tag :a :title "Starred Threads"}) + (check-for d {:xpath (xpath-contains-string ".message .content" + "Welcome to #new-thread")}) + (check-requests d))) + + (testing "Searching for threads" + (testing "Search for a word" + (e/fill-human d [{:class "search-bar"} {:tag :input :type "text"}] + "Welcome") + (check-requests d)) + + (testing "See results" + (check-for d {:xpath (xpath-contains-string ".content .description" + "Displaying 1/1")}) + (check-for d {:xpath (xpath-contains-string ".message .content" + "Welcome to #new-thread")}))) + (testing "Unsubscribing from tags" + (testing "Unsubscribe from a tag" + (e/click d {:tag :a :title "Inbox"}) + (e/click d {:xpath (css->xpath ".close")}) + (e/mouse-move-to d {:tag :div :class "more"}) + (e/click d {:tag :a :fn/has-string "Manage Subscriptions"}) + (e/js-execute d "[... document.querySelectorAll('.tag .name')].filter(el => el.outerText === '#NEW-THREAD')[0].parentElement.parentElement.parentElement.querySelector('a.button').click()") + (check-requests d)) + + (testing "Start a thread with that tag as @foo" + (check-for d {:tag :div :class "more"}) + (e/mouse-move-to d {:tag :div :class "more"}) + (e/click d {:tag :a :fn/has-string "Log Out"}) + (check-for d {:tag :input :type "email"}) + (e/fill d {:tag :input :type "email"} "foo@example.com") + (e/fill d {:tag :input :type "password"} "foofoofoo") + (e/click d {:tag :button :type "submit" :fn/has-string "Log in to Braid"}) + (e/click-visible d {:tag :a :fn/has-class "group" :title "Braid"}) + (e/click d {:tag :button :class "new-thread"}) + (e/fill-human d {:tag :textarea :index 1} "#new-thread is super cool!") + (press-key d e.keys/enter)) + + (testing "Can't see the new thread with unsubscribed tag as @bar" + (check-for d {:tag :div :class "more"}) + (e/mouse-move-to d {:tag :div :class "more"}) + (e/click d {:tag :a :fn/has-string "Log Out"}) + (check-for d {:tag :input :type "email"}) + (e/fill d {:tag :input :type "email"} "bar@example.com") + (e/fill d {:tag :input :type "password"} "barbarbar") + (e/click d {:tag :button :type "submit" :fn/has-string "Log in to Braid"}) + (e/click-visible d {:tag :a :fn/has-class "group" :title "Braid"}) + (check-not d {:xpath (xpath-contains-string ".message .content" + "#new-thread is super cool!")}) + (check-requests d))))))