Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues 671 and 672 #673

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
29 changes: 26 additions & 3 deletions doc/01-user-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions env/test/resources/static/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,13 @@ <h3>Find multiple elements</h3>
<h3>Find multiple elements nested</h3>

<div id="find-elements-nested">
<div class="nested">
<div class="target">1</div>
<div class="target">2</div>
<div id="find-elements-nested-1" class="nested">
<div id="find-elements-nested-2" class="target">1</div>
<div id="find-elements-nested-3" class="target">2</div>
</div>
<div class="nested">
<div class="target">3</div>
<div class="target">4</div>
<div id="find-elements-nested-4" class="nested">
<div id="find-elements-nested-5" class="target">3</div>
<div id="find-elements-nested-6" class="target">4</div>
</div>
</div>

Expand Down
34 changes: 26 additions & 8 deletions src/etaoin/api.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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))

Expand All @@ -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`.
Expand Down
92 changes: 80 additions & 12 deletions test/etaoin/api_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
Loading