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
+
-
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"