Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"shadow-cljs": "3.3.6"
},
"dependencies": {
"@testing-library/react": "16.3.2"
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1"
}
}
3 changes: 2 additions & 1 deletion shadow-cljs.edn
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{:dependencies [[reagent "2.0.1"]]
{:dependencies [[reagent "2.0.1"]
[funcool/promesa "11.0.678"]]
:source-paths ["src/main" "src/test"]
:builds
{:test {:target :node-test
Expand Down
30 changes: 30 additions & 0 deletions src/main/react_testing_library_cljs/async.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
(ns react-testing-library-cljs.async)

(defmacro deftest-async
"Like `cljs.test/deftest` but for async tests that return a Promise.
Wraps the body in `promesa.core/do`, so each top-level form is awaited
before the next runs. Automatically calls `done` when the Promise resolves.

Requires `promesa` on the classpath.

(deftest-async my-test
(render-el (react/createElement \"input\" #js {:placeholder \"type here\"}))
(user-event/type (user-event/setup) (screen/get-by-placeholder-text \"type here\") \"hello\")
(is (= \"hello\" (.-value (screen/get-by-placeholder-text \"type here\")))))

Use `promesa.core/do` explicitly when async calls share a `let` binding:

(deftest-async my-test
(let [clicks (atom 0)]
(render-el (react/createElement \"button\" #js {:onClick #(swap! clicks inc)} \"Click\"))
(p/do
(user-event/click (user-event/setup) (screen/get-by-role \"button\"))
(is (= 1 @clicks)))))"
[name & body]
`(cljs.test/deftest ~name
(cljs.test/async done#
(-> (promesa.core/do ~@body)
(.then done#)
(.catch (fn [err#]
(cljs.test/is (nil? err#) (.-message err#))
(done#)))))))
2 changes: 2 additions & 0 deletions src/main/react_testing_library_cljs/async.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
(ns react-testing-library-cljs.async
(:require-macros [react-testing-library-cljs.async]))
136 changes: 136 additions & 0 deletions src/main/react_testing_library_cljs/user_event.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
(ns react-testing-library-cljs.user-event
"ClojureScript wrapper for @testing-library/user-event.

Simulates real user interactions by firing the full sequence of events
(focus, keyboard, pointer, input, etc.) rather than a single synthetic event.

Refer to https://testing-library.com/docs/user-event/intro for more details."
(:require
["@testing-library/user-event" :default userEvent]))

(defn setup
"Creates a UserEvent instance for simulating user interactions.
Should be called once per test. All interactions on the instance share
browser state (clipboard, keyboard, pointer, etc.).

All methods on the returned instance return a Promise.

See [userEvent.setup](https://testing-library.com/docs/user-event/setup)."
([] (.setup userEvent))
([options] (.setup userEvent (clj->js options))))

(defn click
"Clicks an element.

See [click](https://testing-library.com/docs/user-event/convenience/#click)."
([user element] (.click user element))
([user element options] (.click user element (clj->js options))))

(defn dbl-click
"Double-clicks an element.

See [dblClick](https://testing-library.com/docs/user-event/convenience/#dblclick)."
([user element] (.dblClick user element))
([user element options] (.dblClick user element (clj->js options))))

(defn triple-click
"Triple-clicks an element (selects all text in an input).

See [tripleClick](https://testing-library.com/docs/user-event/convenience/#tripleclick)."
([user element] (.tripleClick user element))
([user element options] (.tripleClick user element (clj->js options))))

(defn hover
"Moves the pointer to an element.

See [hover](https://testing-library.com/docs/user-event/convenience/#hover)."
([user element] (.hover user element))
([user element options] (.hover user element (clj->js options))))

(defn unhover
"Moves the pointer away from an element.

See [unhover](https://testing-library.com/docs/user-event/convenience/#unhover)."
([user element] (.unhover user element))
([user element options] (.unhover user element (clj->js options))))

(defn type
"Types text into an element character by character, simulating the full
keyboard event sequence for each character.

See [type](https://testing-library.com/docs/user-event/utility/#type)."
([user element text] (.type user element text))
([user element text options] (.type user element text (clj->js options))))

(defn clear
"Selects all text in an element and deletes it.

See [clear](https://testing-library.com/docs/user-event/utility/#clear)."
[user element] (.clear user element))

(defn select-options
"Selects one or more options in a `<select>` element.
`values` can be a single value or a vector of values (option text, value, or element).

See [selectOptions](https://testing-library.com/docs/user-event/utility/#selectoptions)."
([user element values] (.selectOptions user element (clj->js values)))
([user element values options] (.selectOptions user element (clj->js values) (clj->js options))))

(defn deselect-options
"Deselects one or more options in a `<select multiple>` element.
`values` can be a single value or a vector of values (option text, value, or element).

See [deselectOptions](https://testing-library.com/docs/user-event/utility/#deselectoptions)."
([user element values] (.deselectOptions user element (clj->js values)))
([user element values options] (.deselectOptions user element (clj->js values) (clj->js options))))

(defn upload
"Simulates uploading a file via a file input.
`files` can be a single File or a vector of Files.

See [upload](https://testing-library.com/docs/user-event/utility/#upload)."
([user element files] (.upload user element files))
([user element files options] (.upload user element files (clj->js options))))

(defn tab
"Presses the Tab key, moving focus to the next focusable element.

See [tab](https://testing-library.com/docs/user-event/convenience/#tab)."
([user] (.tab user))
([user options] (.tab user (clj->js options))))

(defn keyboard
"Simulates keyboard events for the given key descriptor string
(e.g. `\"{Enter}\"`, `\"a\"`, `\"{{a}}\"`).

See [keyboard](https://testing-library.com/docs/user-event/keyboard)."
([user text] (.keyboard user text))
([user text options] (.keyboard user text (clj->js options))))

(defn pointer
"Simulates pointer device interactions.

See [pointer](https://testing-library.com/docs/user-event/pointer)."
([user input] (.pointer user (clj->js input)))
([user input options] (.pointer user (clj->js input) (clj->js options))))

(defn copy
"Copies the current selection to the clipboard.

See [copy](https://testing-library.com/docs/user-event/clipboard/#copy)."
([user] (.copy user))
([user options] (.copy user (clj->js options))))

(defn cut
"Cuts the current selection to the clipboard.

See [cut](https://testing-library.com/docs/user-event/clipboard/#cut)."
([user] (.cut user))
([user options] (.cut user (clj->js options))))

(defn paste
"Pastes clipboard contents into the focused element.

See [paste](https://testing-library.com/docs/user-event/clipboard/#paste)."
([user] (.paste user))
([user options] (.paste user (clj->js options))))
69 changes: 69 additions & 0 deletions src/test/react_testing_library_cljs/user_event_promesa_test.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
(ns react-testing-library-cljs.user-event-promesa-test
(:require
[cljs.test :refer [is]]
["@testing-library/react" :as rtl]
["react" :as react]
[promesa.core :as p]
[react-testing-library-cljs.async :refer-macros [deftest-async]]
[react-testing-library-cljs.screen :as screen]
[react-testing-library-cljs.user-event :as user-event]))

(defn- render-el [element]
(rtl/render element))

(deftest-async click-test
(rtl/cleanup)
(let [clicks (atom 0)]
(render-el (react/createElement "button"
#js {:onClick #(swap! clicks inc)}
"Click me"))
(p/do
(user-event/click (user-event/setup) (screen/get-by-role "button"))
(is (= 1 @clicks)))))

(deftest-async type-test
(rtl/cleanup)
(render-el (react/createElement "input"
#js {:placeholder "type here"
:defaultValue ""}))
(user-event/type (user-event/setup) (screen/get-by-placeholder-text "type here") "hello")
(is (= "hello" (.-value (screen/get-by-placeholder-text "type here")))))

(deftest-async clear-test
(rtl/cleanup)
(render-el (react/createElement "input"
#js {:placeholder "to clear"
:defaultValue "existing text"}))
(user-event/clear (user-event/setup) (screen/get-by-placeholder-text "to clear"))
(is (= "" (.-value (screen/get-by-placeholder-text "to clear")))))

(deftest-async tab-test
(rtl/cleanup)
(render-el (react/createElement "div" nil
(react/createElement "input" #js {:placeholder "first"})
(react/createElement "input" #js {:placeholder "second"})))
(.focus (screen/get-by-placeholder-text "first"))
(user-event/tab (user-event/setup))
(is (= (screen/get-by-placeholder-text "second") (.-activeElement js/document))))

(deftest-async keyboard-test
(rtl/cleanup)
(let [submitted (atom false)]
(render-el (react/createElement "form"
#js {:onSubmit (fn [e]
(.preventDefault e)
(reset! submitted true))}
(react/createElement "input" #js {:placeholder "press enter"})))
(.focus (screen/get-by-placeholder-text "press enter"))
(p/do
(user-event/keyboard (user-event/setup) "{Enter}")
(is (true? @submitted)))))

(deftest-async select-options-test
(rtl/cleanup)
(render-el (react/createElement "select"
#js {:defaultValue ""}
(react/createElement "option" #js {:value "a"} "Option A")
(react/createElement "option" #js {:value "b"} "Option B")))
(user-event/select-options (user-event/setup) (screen/get-by-role "combobox") "Option A")
(is (= "a" (.-value (screen/get-by-role "combobox")))))
94 changes: 94 additions & 0 deletions src/test/react_testing_library_cljs/user_event_test.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
(ns react-testing-library-cljs.user-event-test
(:require
[cljs.test :refer [deftest is async]]
["@testing-library/react" :as rtl]
["react" :as react]
[react-testing-library-cljs.screen :as screen]
[react-testing-library-cljs.user-event :as user-event]))

(defn- render-el [element]
(rtl/render element))

(deftest click-test
(async done
(rtl/cleanup)
(let [clicks (atom 0)]
(render-el (react/createElement "button"
#js {:onClick #(swap! clicks inc)}
"Click me"))
(let [user (user-event/setup)]
(-> (user-event/click user (screen/get-by-role "button"))
(.then (fn []
(is (= 1 @clicks))
(done))))))))

(deftest type-test
(async done
(rtl/cleanup)
(render-el (react/createElement "input"
#js {:placeholder "type here"
:defaultValue ""}))
(let [user (user-event/setup)
input (screen/get-by-placeholder-text "type here")]
(-> (user-event/type user input "hello")
(.then (fn []
(is (= "hello" (.-value input)))
(done)))))))

(deftest clear-test
(async done
(rtl/cleanup)
(render-el (react/createElement "input"
#js {:placeholder "to clear"
:defaultValue "existing text"}))
(let [user (user-event/setup)
input (screen/get-by-placeholder-text "to clear")]
(-> (user-event/clear user input)
(.then (fn []
(is (= "" (.-value input)))
(done)))))))

(deftest tab-test
(async done
(rtl/cleanup)
(render-el (react/createElement "div" nil
(react/createElement "input" #js {:placeholder "first"})
(react/createElement "input" #js {:placeholder "second"})))
(let [user (user-event/setup)
first-input (screen/get-by-placeholder-text "first")
second-input (screen/get-by-placeholder-text "second")]
(.focus first-input)
(-> (user-event/tab user)
(.then (fn []
(is (= second-input (.-activeElement js/document)))
(done)))))))

(deftest keyboard-test
(async done
(rtl/cleanup)
(let [submitted (atom false)]
(render-el (react/createElement "form"
#js {:onSubmit (fn [e]
(.preventDefault e)
(reset! submitted true))}
(react/createElement "input" #js {:placeholder "press enter"})))
(let [user (user-event/setup)]
(.focus (screen/get-by-placeholder-text "press enter"))
(-> (user-event/keyboard user "{Enter}")
(.then (fn []
(is (true? @submitted))
(done))))))))

(deftest select-options-test
(async done
(rtl/cleanup)
(render-el (react/createElement "select"
#js {:defaultValue ""}
(react/createElement "option" #js {:value "a"} "Option A")
(react/createElement "option" #js {:value "b"} "Option B")))
(let [user (user-event/setup)
select (screen/get-by-role "combobox")]
(-> (user-event/select-options user select "Option A")
(.then (fn []
(is (= "a" (.-value select)))
(done)))))))