diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 6aae867..f1c850e 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -11,4 +11,6 @@ etaoin.impl.util/with-tmp-file etaoin.impl.util/with-tmp-file}} :linters {:deprecated-var {:exclude {etaoin.api/child {:namespaces [etaoin.api-test]} - etaoin.api/children {:namespaces [etaoin.api-test]}}}}} + etaoin.api/children {:namespaces [etaoin.api-test]}}} + :unresolved-symbol + {:exclude [(clojure.test/is [thrown+? thrown+-with-msg?])]}}} diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 6beef14..391ffb2 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -34,6 +34,8 @@ A release with an intentional breaking changes is marked with: ** {issue}663[#663]: `query` throws a more accurate exception with a more accurate error message when provided with an empty query vector. ({person}dgr[@dgr]) * Docs ** {issue}656[#656]: Correctly describe behavior when query's parameter is a string. The User Guide and `query` doc strings say that a string passed to `query` is interpreted as an XPath expression. In fact, `query` interprets this as either XPath or CSS depending on the setting of the driver's `:locator` parameter, which can be changed. ({person}dgr[@dgr]) +* Quality +** {issue}640[#650]: Significantly improved test coverage. ({person}dgr[@dgr]) == v1.1.41 [minor breaking] - 2024-08-14 [[v1.1.41]] diff --git a/env/test/resources/static/test.html b/env/test/resources/static/test.html index 89554cd..de569b3 100644 --- a/env/test/resources/static/test.html +++ b/env/test/resources/static/test.html @@ -169,8 +169,8 @@

Cookies section

Element property section

Inner HTML
-

Find element

-
target-1
+

Find element

+
target-1
target-2
@@ -181,6 +181,23 @@

Find element

+
DIV with strange attribute
+

:fn/* tests

+
multiple classes
+
Test Indexing
+
    +
  1. ordered 1
  2. +
  3. ordered 2
  4. +
  5. ordered 3
  6. +
+ Link to GitHub +
Enabled/Disabled elements
+ Checkbox 1 - Enabled
+ Checkbox 2 - Disabled
+ Checkbox 3 - Enabled
+
+ +

Find multiple elements

diff --git a/test/etaoin/api_test.clj b/test/etaoin/api_test.clj index c3fa5fc..522bb08 100644 --- a/test/etaoin/api_test.clj +++ b/test/etaoin/api_test.clj @@ -13,7 +13,8 @@ [etaoin.impl.client :as client] [etaoin.keys :as k] [etaoin.test-report :as test-report] - [slingshot.slingshot :refer [try+]]) + [slingshot.slingshot :refer [try+]] + [slingshot.test]) (:import [java.net ServerSocket])) (defn numeric? [val] @@ -128,6 +129,33 @@ report-browsers test-server) +(deftest test-browser-conditionals + (testing "Chrome conditionals" + (e/when-chrome *driver* + (is (e/driver? *driver* :chrome))) + (e/when-not-chrome *driver* + (is (not (e/driver? *driver* :chrome))))) + (testing "Firefox conditionals" + (e/when-firefox *driver* + (is (e/driver? *driver* :firefox))) + (e/when-not-firefox *driver* + (is (not (e/driver? *driver* :firefox))))) + (testing "Safari conditionals" + (e/when-safari *driver* + (is (e/driver? *driver* :safari))) + (e/when-not-safari *driver* + (is (not (e/driver? *driver* :safari))))) + (testing "Edge conditionals" + (e/when-edge *driver* + (is (e/driver? *driver* :edge))) + (e/when-not-edge *driver* + (is (not (e/driver? *driver* :edge))))) + (testing "Headless conditionals" + (e/when-headless *driver* + (is (e/headless? *driver*))) + (e/when-not-headless *driver* + (is (not (e/headless? *driver*)))))) + (deftest test-navigation (is (= (test-server-url "test.html") (e/get-url *driver*)) "initial page") (e/go *driver* (test-server-url "test2.html")) @@ -158,27 +186,78 @@ (is (e/selected? *driver* :vehicle2)) (is (e/selected? *driver* :vehicle3))) +(deftest test-submit + (doto *driver* + (e/fill-multi {:simple-input 1 + :simple-password 2 + :simple-textarea 3}) + (e/submit :simple-input) + ;; Both Safari and Firefox need a slight delay here before the URL + ;; corresponding to the submitted form is valid. Chrome and Edge + ;; seem to do OK without. As of Aug 28, 2024. + (e/when-safari (e/wait 3)) + (e/when-firefox (e/wait 3)) + (-> e/get-url + (str/ends-with? "?login=1&password=2&message=3") + is))) + (deftest test-input (testing "fill multiple inputs" + ;; Test with map form (doto *driver* (e/fill-multi {:simple-input 1 :simple-password 2 :simple-textarea 3}) (e/click :simple-submit) (e/when-safari (e/wait 3)) + (-> e/get-url + (str/ends-with? "?login=1&password=2&message=3") + is)) + ;; Test with vector form + (doto *driver* + (e/fill-multi [:simple-input 1 + :simple-password 2 + :simple-textarea 3]) + (e/click :simple-submit) + (e/when-safari (e/wait 3)) (-> e/get-url (str/ends-with? "?login=1&password=2&message=3") is))) + (testing "fill-multi bad inputs" + (is (thrown+? [:type :etaoin/argument] + (e/fill-multi *driver* #{:set :is :not :allowed}))) + (is (thrown+? [:type :etaoin/argument] + (e/fill-multi *driver* '(:list :is :not :allowed)))) + (is (thrown+? [:type :etaoin/argument] + (e/fill-multi *driver* [:vector :with :odd :length :is :not :allowed])))) (testing "fill human multiple inputs" (doto *driver* + ;; Test with map form (e/fill-human-multi {:simple-input "login" :simple-password "123" :simple-textarea "text"}) (e/click :simple-submit) (e/when-safari (e/wait 3)) + (-> e/get-url + (str/ends-with? "?login=login&password=123&message=text") + is)) + (doto *driver* + ;; Test with vector form + (e/fill-human-multi [:simple-input "login" + :simple-password "123" + :simple-textarea "text"]) + (e/click :simple-submit) + (e/when-safari (e/wait 3)) (-> e/get-url (str/ends-with? "?login=login&password=123&message=text") is))) + (testing "fill-human-multi bad inputs" + (is (thrown+? [:type :etaoin/argument] + (e/fill-multi *driver* #{:set :is :not :allowed}))) + (is (thrown+? [:type :etaoin/argument] + (e/fill-multi *driver* '(:list :is :not :allowed)))) + (is (thrown+? [:type :etaoin/argument] + (e/fill-multi *driver* [:vector :with :odd :length :is :not :allowed])))) (testing "fill multiple vars" (doto *driver* (e/fill :simple-input 1 "test" 2 \space \A) @@ -600,7 +679,7 @@ (is (= init-handle (e/get-window-handle *driver*)) "wrapped around to original window"))) (deftest test-maximize - (when-not (e/headless? *driver*) ;; skip for headless + (e/when-not-headless *driver* ;; skip for headless (e/set-window-position *driver* 2 2) (let [orig-rect (e/get-window-rect *driver*) target-rect (-> orig-rect @@ -749,22 +828,118 @@ (e/set-hash "goodbye") (-> e/get-url (str/ends-with? "/test.html#goodbye") is)))) -(deftest test-find-element - (let [text (e/get-element-text *driver* {:class :target})] - (is (= text "target-1"))) - (let [text (e/get-element-text *driver* [{:class :foo} - {:class :target}])] - (is (= text "target-2"))) - (e/with-xpath *driver* - (let [text (e/get-element-text *driver* ".//div[@class='target'][1]")] - (is (= text "target-1")))) - (let [text (e/get-element-text *driver* {:css ".target"})] - (is (= text "target-1"))) - (let [q [{:css ".bar"} ".//div[@class='inside']" {:tag :span}] - text (e/get-element-text *driver* q)] - (is (= text "target-3")))) - -(deftest test-find-elements-more +(deftest test-query + (testing "finding an element by id keyword" + (let [el (e/query *driver* :find-element-by-id)] + (is (= "target-1" (e/get-element-text-el *driver* el))))) + (testing "XPath and CSS string syntax" + (e/with-xpath *driver* + (let [el (e/query *driver* ".//div[@class='target'][1]")] + (is (= "target-1" (e/get-element-text-el *driver* el))))) + (e/with-css *driver* + (let [el (e/query *driver* ".bar .deep .inside span") ] + (is (= "target-3" (e/get-element-text-el *driver* el)))))) + (testing "XPath and CSS map syntax" + (let [el (e/query *driver* {:xpath ".//*[@class='target']"})] + (is (= "target-1" (e/get-element-text-el *driver* el)))) + (let [el (e/query *driver* {:css ".target"})] + (is (= "target-1" (e/get-element-text-el *driver* el))))) + (testing "map syntax" + ;; 1. tags + (testing "tags" + (let [el (e/query *driver* {:tag :h3 :id :find-element})] + (is (= "Find element" (e/get-element-text-el *driver* el))))) + ;; 2. class + (testing "class" + (let [el (e/query *driver* {:class :target})] + (is (= "target-1" (e/get-element-text-el *driver* el))))) + ;; 3. random attributes + (testing "random attributes" + (let [el (e/query *driver* {:strangeattribute :foo})] + (is (= "DIV with strange attribute" (e/get-element-text-el *driver* el))))) + ;; 4. :fn/* + (testing ":fn/* functions" + ;; :index and :fn/index + (let [el (e/query *driver* {:class :list :index 3})] ; deprecated syntax + (is (= "ordered 3" (e/get-element-text-el *driver* el)))) + (let [el (e/query *driver* {:class :list :fn/index 3})] ; new syntax + (is (= "ordered 3" (e/get-element-text-el *driver* el)))) + ;; :fn/text + (let [el (e/query *driver* {:fn/text "multiple classes"})] + (is (= "multiple-classes" (e/get-element-attr-el *driver* el "id")))) + ;; :fn/has-text + (let [el (e/query *driver* {:fn/has-text "ple cla"})] ; pick out the middle + (is (= "multiple-classes" (e/get-element-attr-el *driver* el "id")))) + ;; :fn/has-string + (let [el (e/query *driver* {:tag :ol :fn/has-string "ordered 3"})] + (is (= "ordered-list" (e/get-element-attr-el *driver* el "id")))) + ;; :fn/has-class + (let [el (e/query *driver* {:fn/has-class "ol-class1"})] + (is (= "ordered-list" (e/get-element-attr-el *driver* el "id")))) + ;; :fn/has-classes + ;; verify that order doesn't matter + (let [elx (e/query *driver* {:fn/has-classes [:ol-class1 :ol-class2]}) + ely (e/query *driver* {:fn/has-classes [:ol-class2 :ol-class1]}) + elz (e/query *driver* :ordered-list)] + (is (= elx ely elz))) + ;; :fn/link + (let [el (e/query *driver* {:fn/link "https://www.github.com/"})] + (is (= "Link to GitHub" (e/get-element-text-el *driver* el)))) + ;; :fn/enabled + (let [el (e/query *driver* [:enabled-disabled {:type :checkbox :fn/enabled true}])] + (is (= "checkbox-1" (e/get-element-attr-el *driver* el "id")))) + (let [el (e/query *driver* [:enabled-disabled {:type :checkbox :fn/enabled false}])] + (is (= "checkbox-2" (e/get-element-attr-el *driver* el "id")))) + (let [el (e/query *driver* [:enabled-disabled {:type :checkbox :fn/enabled true :fn/index 2}])] + (is (= "checkbox-3" (e/get-element-attr-el *driver* el "id")))) + ;; :fn/disabled + (let [el (e/query *driver* [:enabled-disabled {:type :checkbox :fn/disabled false}])] + (is (= "checkbox-1" (e/get-element-attr-el *driver* el "id")))) + (let [el (e/query *driver* [:enabled-disabled {:type :checkbox :fn/disabled true}])] + (is (= "checkbox-2" (e/get-element-attr-el *driver* el "id")))) + (let [el (e/query *driver* [:enabled-disabled {:type :checkbox :fn/disabled false :fn/index 2}])] + (is (= "checkbox-3" (e/get-element-attr-el *driver* el "id")))))) + (testing "vector syntax" + ;; TODO: should check vectors with length 1, 2, and 3. + (e/with-xpath *driver* ; force XPath because we use a string + (let [el (e/query *driver* [{:css ".bar"} ".//div[@class='inside']" {:tag :span}])] + (is (= "target-3" (e/get-element-text-el *driver* el))))) + (let [el (e/query *driver* [{:class :foo} {:class :target}])] + (is (= "target-2" (e/get-element-text-el *driver* el))))) + (testing "variable arguments syntax" + ;; Same as vector syntax but just provided as separate arguments to `query` + (e/with-xpath *driver* + (let [el (e/query *driver* {:css ".bar"} ".//div[@class='inside']" {:tag :span})] + (is (= "target-3" (e/get-element-text-el *driver* el))))) + (let [el (e/query *driver* {:class :foo} {:class :target})] + (is (= "target-2" (e/get-element-text-el *driver* el))))) + (testing "negative test cases" + ;; TODO: + ;; 1. searching for nothing + (testing "zero-length vector queries" + ;; 1. pass a vector of length 0 to query + (is (thrown+? [:type :etaoin/argument] (e/query *driver* [])))) + ;; 2. searching for an element that can't be found + (testing "querying for missing elements" + ;; 2a. searching for a missing element with missing ID + (is (thrown+? [:type :etaoin/http-error] (e/query *driver* :missing-element))) + (is (thrown+? [:type :etaoin/http-error] (e/query *driver* [:missing-element]))) + ;; 2a. element not found in middle of a vector query + (is (thrown+? [:type :etaoin/http-error] (e/query *driver* [{:css ".bar"} + :missing-element + {:tag :span}]))) + ;; 2b. element not found at the end of a vector query + (is (thrown+? [:type :etaoin/http-error] (e/query *driver* [{:css ".bar"} + {:tag :div :class :inside} + :missing-element])))) + ;; 3. malformed XPath + ;; 4. malformed CSS + ;; 5. query isn't a string, map, or vector. Perhaps a list and set. + ;; 6. bad :fn/... keywords + ;; 7. vector queries with vector elements (vectors in vectors) + )) + +(deftest test-query-all (testing "simple case" (let [q {:class :find-elements-target} elements (e/query-all *driver* q)] @@ -777,7 +952,22 @@ texts (for [el elements] (e/get-element-text-el *driver* el))] (is (= (count elements) 2)) - (is (= texts ["1" "2"]))))) + (is (= texts ["1" "2"])))) + (testing "returning multiple elements via XPath" + (let [q {:xpath ".//div[@id='operate-multiple-elements']//*"} + elements (e/query-all *driver* q) + tag-names (for [el elements] + (str/lower-case (e/get-element-tag-el *driver* el)))] + (is (= (vec tag-names) + ["div" "b" "p" "span"]))))) + +(deftest test-switch-default-locator + (testing "xpath locator" + (let [driver (e/use-xpath *driver*)] + (is (= "target-1" (e/get-element-text driver ".//*[@class='target']"))))) + (testing "css locator" + (let [driver (e/use-css *driver*)] + (is (= "target-1" (e/get-element-text driver ".target")))))) (deftest test-fn-index (testing ":fn/index" @@ -786,15 +976,6 @@ (e/get-element-text-el *driver*)))] (is (= items ["One" "Two" "Three" "Four" "Five"]))))) -(deftest test-multiple-elements - (testing "tag names" - (let [q {:xpath ".//div[@id='operate-multiple-elements']//*"} - elements (e/query-all *driver* q) - tag-names (for [el elements] - (str/lower-case (e/get-element-tag-el *driver* el)))] - (is (= (vec tag-names) - ["div" "b" "p" "span"]))))) - (deftest test-query-tree (let [url (test-server-url "test2.html") _ (e/go *driver* url)