diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 5c24dc2..e572169 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -36,6 +36,8 @@ A release with an intentional breaking changes is marked with: ** {issue}668[#668]: Throw an exception for unknown `:fn/*` keywords in map queries. * 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]) +** {issue}671[#671]: The `query` function now supports sequentials of all types (vectors, lists, seqs, etc.) with the same behavior as the previous vector-only syntax. ({person}dgr[@dgr]) +** {issue}672[#672]: Queries support hierarchical arrangements of sequences (sequences in sequences), which are flattened into a single sequence before query traversal. ({person}dgr[@dgr]) * Quality ** {issue}640[#640]: Significantly improved test coverage. ({person}dgr[@dgr]) diff --git a/doc/01-user-guide.adoc b/doc/01-user-guide.adoc index ccb2f92..55377c0 100644 --- a/doc/01-user-guide.adoc +++ b/doc/01-user-guide.adoc @@ -728,10 +728,10 @@ Here are some examples of the map syntax: ;; => true ---- -=== Vector Syntax Queries +=== Sequential Queries -A query can be a vector of any valid query expressions. -For vector queries, every expression matches the output from the previous expression. +A query can be a sequential collection (e.g., vectors, lists, seqs, etc.) of any valid query expressions, including other sequential collections. +For sequential queries, every expression matches the output from the previous expression. A simple, somewhat contrived, example: @@ -759,6 +759,29 @@ TIP: Reminder: the leading dot in an XPath expression means starting at the curr ;; => "link 2 (active)" ---- +You can also create a query from multiple pieces and pull them together by wrapping them in a sequence. +For instance, you might have a common prefix in the query that leads to a particular component in the DOM tree and you want to use that repeatedly in multiple queries. + +[source,clojure] +---- +;; first we define our prefix +(def prefix [{:class :some-links} {:tag :ul}]) +;; now use it in a query +;; this query structure is flattened to +;; [{:class :some-links} {:tag :ul} {:tag :a}] +(e/get-element-text driver [prefix {:tag :a}]) +;; => "link 1" +---- + +You can also create portions of the query structure using other sequence functions and leave it to `query` to flatten everything for you. + +[source,clojure] +---- +(e/get-element-text driver [{:class :some-links} + (filter identity [{:tag :ul} nil {:tag :a} nil])]) +;; => "link 1" +---- + === Advanced Queries ==== Querying the _nth_ Element Matched diff --git a/env/test/resources/static/test.html b/env/test/resources/static/test.html index de569b3..843dece 100644 --- a/env/test/resources/static/test.html +++ b/env/test/resources/static/test.html @@ -215,13 +215,13 @@

Find multiple elements

Find multiple elements nested

-
-
1
-
2
+
+
1
+
2
-
-
3
-
4
+
+
3
+
4
diff --git a/src/etaoin/api.clj b/src/etaoin/api.clj index 8daa0bc..24c2c76 100644 --- a/src/etaoin/api.clj +++ b/src/etaoin/api.clj @@ -614,14 +614,26 @@ - any other map is converted to an XPath expression: - `{:tag :div}` - is equivalent to XPath: `\".//div\"` - - multiple of the above (wrapped in a vector or not). + - multiple of the above (wrapped in a sequential -- vector, list, seq -- or not). The result of each search anchors the search for the next, effectively providing a path through the DOM (though you do not have to specify each and every point in the path). - `{:tag :div} \".//input[@id='uname']\"` - `[{:tag :div} \".//input[@id='uname']\"]` + - `'({:tag :div} \".//input[@id='uname']\")` Returns the final element's unique identifier, or throws if any element - is not found. + along the path is not found. + + - Note that sequences of sequences of arbitrary hierarchy are also + supported. Before traversing the path, `query` flattens the + hierarchy into a single linear sequence. This can allow for + sub-paths within the DOM to be stored in variables and simply + included in the path vector. + - e.g., `[[:id1 :id2 ] [ :id3 :id4 ]]` is flattened to + `[:id1 :id2 :id3 :id4]`. + - If `prefix` is bound to `[:id1 :id2]`, then Clojure will convert + `[prefix :id3 :id4]` to `[[:id1 :id2] :id3 :id4]` and `query` + will then flatten this to `[:id1 :id2 :id3 :id4]`. See [Selecting Elements](/doc/01-user-guide.adoc#querying) for more details. @@ -635,10 +647,10 @@ (= q :active) (get-active-element driver) - (vector? q) + (sequential? q) (if (empty? q) (throw+ {:type :etaoin/argument - :message "Vector query must be non-empty" + :message "Queries must be non-empty" :q q}) (apply query driver q)) @@ -647,10 +659,16 @@ (find-element* driver loc term)))) ([driver q & more] - (letfn [(folder [el q] - (let [[loc term] (query/expand driver q)] - (find-element-from* driver el loc term)))] - (reduce folder (query driver q) more)))) + (let [[q & more :as full-q] (flatten (cons q more))] + (if (empty? full-q) + (throw+ {:type :etaoin/argument + :message "Queries must be non-empty" + :q full-q}) + (reduce (fn [el q] + (let [[loc term] (query/expand driver q)] + (find-element-from* driver el loc term))) + (query driver q) + more))))) (defn query-all "Use `driver` to return a vector of all elements on current page matching `q`. diff --git a/test/etaoin/api_test.clj b/test/etaoin/api_test.clj index 8d3bde1..5b136a9 100644 --- a/test/etaoin/api_test.clj +++ b/test/etaoin/api_test.clj @@ -900,21 +900,91 @@ (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. + ;; vector length 0 + (is (thrown+? [:type :etaoin/argument] (e/query *driver* []))) + ;; vector length 1 + (let [el (e/query *driver* [:find-elements-nested])] + (is (= "div" (str/lower-case (e/get-element-tag-el *driver* el))))) + ;; vector length 2 + (let [el (e/query *driver* [{:class :foo} {:class :target}])] + (is (= "target-2" (e/get-element-text-el *driver* el)))) + ;; vector length 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))))) + (is (= "target-3" (e/get-element-text-el *driver* el)))))) + (testing "sequences syntax" + ;; sequence length 0 + (is (thrown+? [:type :etaoin/argument] (e/query *driver* '()))) + ;; sequence length 1 + (let [el (e/query *driver* '(:find-elements-nested))] + (is (= "div" (str/lower-case (e/get-element-tag-el *driver* el))))) + ;; sequence length 2 + (let [el (e/query *driver* '({:class :foo} {:class :target}))] + (is (= "target-2" (e/get-element-text-el *driver* el)))) + ;; sequence length 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)))))) (testing "variable arguments syntax" - ;; Same as vector syntax but just provided as separate arguments to `query` + ;; Same as vector/sequence syntax but just provided as separate + ;; arguments to `query`. + + ;; arg length 0 + ;; This test is passes, but Eastwood complains about :wrong-arity + ;; and doesn't have a good way to turn off the linter for this + ;; specific line. + ;; (is (thrown? clojure.lang.ArityException (e/query *driver* ,,,))) + + ;; arg length 1 + (let [el (e/query *driver* :find-elements-nested)] + (is (= "div" (str/lower-case (e/get-element-tag-el *driver* el))))) + ;; arg length 2 + (let [el (e/query *driver* {:class :foo} {:class :target})] + (is (= "target-2" (e/get-element-text-el *driver* el)))) + ;; arg length 3 (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))))) + (is (= "target-3" (e/get-element-text-el *driver* el)))))) + (testing "sequences in a sequence" + ;; Just test whether we can find an element or not + ;; 1. [[ ]] + (is (e/query *driver* [[:find-elements-nested]])) + ;; 2. [[ ] [ ]] + (is (e/query *driver* [[:find-elements-nested] [:find-elements-nested-1]])) + ;; 3. [[ ] [ ] [ ]] + (is (e/query *driver* [[:find-elements-nested] + [:find-elements-nested-1] + [:find-elements-nested-2]])) + ;; 4. [[[ ] [ ]] [ ]] + (is (e/query *driver* [[[:find-elements-nested] [:find-elements-nested-1]] + [:find-elements-nested-2]])) + ;; 5. [[ ] [[ ] [ ]]] + (is (e/query *driver* [[:find-elements-nested] + [[:find-elements-nested-1] [:find-elements-nested-2]]])) + (is (e/query *driver* [:find-elements-nested (for [i (range 1 3)] + (-> (str "find-elements-nested-" i) + keyword))])) + ) + (testing "sequences in variable argument syntax" + ;; Just test whether we can find an element or not + ;; 1. [ ] + (is (e/query *driver* [:find-elements-nested])) + ;; 2. [ ] [ ] + (is (e/query *driver* [:find-elements-nested] [:find-elements-nested-1])) + ;; 3. [ ] [ ] [ ] + (is (e/query *driver* + [:find-elements-nested] + [:find-elements-nested-1] + [:find-elements-nested-2])) + ;; 4. [[ ] [ ]] [ ] + (is (e/query *driver* + [[:find-elements-nested] [:find-elements-nested-1]] + [:find-elements-nested-2])) + ;; 5. [ ] [[ ] [ ]] + (is (e/query *driver* + [:find-elements-nested] + [[:find-elements-nested-1] [:find-elements-nested-2]]))) (testing "negative test cases" - ;; TODO: ;; 1. searching for nothing (testing "zero-length vector queries" ;; 1. pass a vector of length 0 to query @@ -938,9 +1008,7 @@ ;; 6. unknown :fn/... keywords (testing "unknown :fn/* keywords" ;; ":fn/indx" is probably a typo and the user really wants ":fn/index" - (is (thrown+? [:type :etaoin/argument] (e/query *driver* {:tag :div :fn/indx 1})))) - ;; 7. vector queries with vector elements (vectors in vectors) - )) + (is (thrown+? [:type :etaoin/argument] (e/query *driver* {:tag :div :fn/indx 1})))))) (deftest test-query-all (testing "simple case"