diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index af354e0a..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,72 +0,0 @@ - -version: 2 -jobs: - build: - docker: - # This Docker image contains a very old firefox and no geckodriver, - # due to Selenium compatibility. Since we don't use Selenium, we take - # care of installing a recent firefox and geckodriver (see below). - - image: circleci/clojure:lein-2.9.3-browsers - - environment: - # PhantomJS requires an OpenSSL config even if it's an empty one, - # else it'll complain about "libssl_conf.so: cannot open shared object file" - # which seems to be a recent bug. - OPENSSL_CONF: /opt/openssl.cnf - - steps: - - checkout - - run: - name: Install phantomjs - command: | - PHANTOMJS_VERSION=phantomjs-2.1.1-linux-x86_64 && \ - wget --quiet --content-disposition https://bitbucket.org/ariya/phantomjs/downloads/$PHANTOMJS_VERSION.tar.bz2 && \ - sudo tar xf $PHANTOMJS_VERSION.tar.bz2 -C /usr/local && \ - sudo ln -s /usr/local/$PHANTOMJS_VERSION/bin/phantomjs /usr/local/bin/phantomjs && \ - rm $PHANTOMJS_VERSION.tar.bz2 - - run: - name: Environment information - command: | - firefox --version - google-chrome --version - chromedriver --version - phantomjs --version - - # We want to share the results of "lein deps" between runs. - # We assume that the maven cache can change only if "project.clj" - # changes (see the checksum in the name of the key below). - - restore_cache: - keys: - - m2-{{ checksum "project.clj" }} - - run: lein deps - - save_cache: - key: m2-{{ checksum "project.clj" }} - paths: - - ~/.m2 - - # For better visibility in the build summary page, we use a separate - # run step per test file. In this case, we don't want to stop the build - # on first failure, so we use the `when` keyword. - - # Note on the ETAOIN_TEST_DRIVERS environment variable: - # The only reason we override the logic to select the drivers is due to - # the following bug: - # When using firefox, at a certain point there is an exception thrown - # from the fixture code. clojure.test is not able to withstand this - # and just chokes. As such, we split the tests between firefox-only - # and everything but firefox to allow to compare them (and see that - # the firefox tests report "ran 0 tests" due to the fixture bug) - - run: lein test2junit - - - store_test_results: - path: target/test2junit - - -# TODO -# -# * Emit test results in XUNIT format so that CircleCI can provide useful -# statistics, see -# https://circleci.com/docs/2.0/configuration-reference/#store_test_results -# -# * (optimization) parallelize the test jobs with CircleCI `workflows` -# diff --git a/.clj-kondo/babashka/fs/config.edn b/.clj-kondo/babashka/fs/config.edn new file mode 100644 index 00000000..23f36094 --- /dev/null +++ b/.clj-kondo/babashka/fs/config.edn @@ -0,0 +1 @@ +{:lint-as {babashka.fs/with-temp-dir clojure.core/let}} diff --git a/.clj-kondo/clj-kondo/slingshot/clj_kondo/slingshot/try_plus.clj b/.clj-kondo/clj-kondo/slingshot/clj_kondo/slingshot/try_plus.clj new file mode 100644 index 00000000..48a4c174 --- /dev/null +++ b/.clj-kondo/clj-kondo/slingshot/clj_kondo/slingshot/try_plus.clj @@ -0,0 +1,44 @@ +(ns clj-kondo.slingshot.try-plus + (:require [clj-kondo.hooks-api :as api])) + +(defn expand-catch [catch-node] + (let [[catch catchee & exprs] (:children catch-node) + catchee-sexpr (api/sexpr catchee)] + (cond (vector? catchee-sexpr) + (let [[selector & exprs] exprs] + (api/list-node + [catch (api/token-node 'Exception) (api/token-node '_e#) + (api/list-node + (list* (api/token-node 'let) + (api/vector-node [selector (api/token-node nil)]) + exprs))])) + :else catch-node))) + +(defn try+ [{:keys [node]}] + (let [children (rest (:children node)) + [body catches] + (loop [body children + body-exprs [] + catches []] + (if (seq body) + (let [f (first body) + f-sexpr (api/sexpr f)] + (if (and (seq? f-sexpr) (= 'catch (first f-sexpr))) + (recur (rest body) + body-exprs + (conj catches (expand-catch f))) + (recur (rest body) + (conj body-exprs f) + catches))) + [body-exprs catches])) + new-node (api/list-node + [(api/token-node 'let) + (api/vector-node + [(api/token-node '&throw-context) (api/token-node nil)]) + (api/token-node '&throw-context) ;; use throw-context to avoid warning + (with-meta (api/list-node (list* (api/token-node 'try) + (concat body catches))) + (meta node))])] + ;; (prn (api/sexpr new-node)) + {:node new-node})) + diff --git a/.clj-kondo/clj-kondo/slingshot/config.edn b/.clj-kondo/clj-kondo/slingshot/config.edn new file mode 100644 index 00000000..446d4f0c --- /dev/null +++ b/.clj-kondo/clj-kondo/slingshot/config.edn @@ -0,0 +1,2 @@ +{:hooks + {:analyze-call {slingshot.slingshot/try+ clj-kondo.slingshot.try-plus/try+}}} diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn new file mode 100644 index 00000000..68dafdd8 --- /dev/null +++ b/.clj-kondo/config.edn @@ -0,0 +1,10 @@ +{:config-paths ^:replace ;; don't adopt any user preferences + ["hooks" ;; keep our internal hooks separate from imported ones + "../resources/clj-kondo.exports/etaoin/etaoin"] ;; include our exported public config + :cljc {:features [:clj]} ;; our bb reader conditionals might make some tools also assume cljs, state otherwise + :hooks + ;; for internal stuff, I'm fine with using macroexpand, our external config uses analyze-call for + ;; finer grained error reporting + {:macroexpand + {etaoin.impl.util/defmethods etaoin.impl.util/defmethods + etaoin.impl.util/with-tmp-file etaoin.impl.util/with-tmp-file}}} diff --git a/.clj-kondo/hooks/etaoin/impl/util.clj_kondo b/.clj-kondo/hooks/etaoin/impl/util.clj_kondo new file mode 100644 index 00000000..6185f8fb --- /dev/null +++ b/.clj-kondo/hooks/etaoin/impl/util.clj_kondo @@ -0,0 +1,13 @@ +(ns etaoin.impl.util) + +(defmacro defmethods + "Declares multimethods in batch. For each dispatch value from + dispatch-vals, creates a new method." + [multifn dispatch-vals & fn-tail] + `(doseq [dispatch-val# ~dispatch-vals] + (defmethod ~multifn dispatch-val# ~@fn-tail))) + +;; essence only for linting +(defmacro with-tmp-file #_{:clj-kondo/ignore [:unused-binding]} [prefix suffix bind & body] + `(let [~bind "somepath"] + ~@body)) diff --git a/.clj-kondo/lread/status-line/config.edn b/.clj-kondo/lread/status-line/config.edn new file mode 100644 index 00000000..ac284abe --- /dev/null +++ b/.clj-kondo/lread/status-line/config.edn @@ -0,0 +1,2 @@ +{:lint-as {lread.status-line/line clojure.tools.logging/infof + lread.status-line/die clojure.tools.logging/infof}} diff --git a/.clj-kondo/rewrite-clj/rewrite-clj/config.edn b/.clj-kondo/rewrite-clj/rewrite-clj/config.edn new file mode 100644 index 00000000..19ecae96 --- /dev/null +++ b/.clj-kondo/rewrite-clj/rewrite-clj/config.edn @@ -0,0 +1,5 @@ +{:lint-as + {rewrite-clj.zip/subedit-> clojure.core/-> + rewrite-clj.zip/subedit->> clojure.core/->> + rewrite-clj.zip/edit-> clojure.core/-> + rewrite-clj.zip/edit->> clojure.core/->>}} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..901d77db --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @lread @borkdude @slipset diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..a1429777 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,96 @@ +name: Test + +on: + push: + branches: ['master'] + pull_request: + +jobs: + setup: + runs-on: ubuntu-latest + + outputs: + tests: ${{ steps.set-tests.outputs.tests }} + + steps: + - uses: actions/checkout@v3 + + - name: Clojure deps cache + uses: actions/cache@v3 + with: + path: | + ~/.m2/repository + ~/.deps.clj + ~/.gitlibs + key: cljdeps-${{ hashFiles('deps.edn', 'bb.edn') }} + restore-keys: cljdeps- + + - name: "Setup Java" + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + + - name: Install Clojure Tools + uses: DeLaGuardo/setup-clojure@5.1 + with: + bb: 'latest' + + # This assumes downloaded deps are same for all OSes + - name: Bring down deps + run: bb download-deps + + - id: set-tests + name: Set test var for matrix + # run test.clj directly instead of via bb task to avoid generic task output + run: echo "::set-output name=tests::$(bb script/test.clj matrix-for-ci --format=json)" + + build: + needs: setup + runs-on: ${{ matrix.os }}-latest + strategy: + fail-fast: false + matrix: + include: ${{fromJSON(needs.setup.outputs.tests)}} + + name: ${{ matrix.desc }} + + steps: + + - uses: actions/checkout@v3 + + - name: Clojure deps cache + uses: actions/cache@v3 + with: + path: | + ~/.m2/repository + ~/.deps.clj + ~/.gitlibs + key: cljdeps-${{ hashFiles('deps.edn', 'bb.edn') }} + restore-keys: cljdeps- + + - name: "Install Missing Windows Bits" + if: ${{ matrix.os == 'windows' }} + run: | + Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" + choco install --no-progress --yes imagemagick + refreshenv + Write-Output "$env:PATH" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: "Setup Java" + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + + - name: Install Clojure Tools + uses: DeLaGuardo/setup-clojure@5.1 + with: + bb: 'latest' + + - name: Tools versions + run: bb tools-versions + + - name: Run Tests + # To see all commands: bb test matrix-for-ci + run: ${{ matrix.cmd }} diff --git a/.gitignore b/.gitignore index 140b6d76..68be2ba7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ /target /classes /checkouts -pom.xml pom.xml.asc *.jar *.class @@ -18,3 +17,13 @@ TAGS *.iml build.xml /.idea + +# ignore cache under .clj-kondo and .lsp +.cache +# VSCode Calva extension +/.calva +# Clojure tools cli classpath cache +/.cpcache +# clj-kondo will copy our own exported config under resources to ./clj-kondo, +# we don't need this duplicate. +/.clj-kondo/etaoin diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc new file mode 100644 index 00000000..07147e89 --- /dev/null +++ b/CHANGELOG.adoc @@ -0,0 +1,264 @@ += Changelog + +A release with an intentional breaking changes is marked with: + +* [breaking] you probably need to change your code +* [minor breaking] you likely don't need to change your code + +== Unreleased [minor breaking] + +Minor Breaking Changes + +* https://github.com/clj-commons/etaoin/issues/412[#412]: Rename `etaoin.keys/num-.` to `etaoin.keys/num-dot`. + +The symbol `num-.` is technically an invalid Clojure symbol and can confuse tooling. + +A grep.app for `num-.` found Etaoin itself as the only user of this var. +If your code uses `etaoin.keys/num-.`, you'll need to rename it to `etaoin.keys/num-dot`. +* https://github.com/clj-commons/etaoin/issues/430[#430]: Declare the public API. +We made what we think is a good guess at what the public Etaoin API is. +The following namespaces are now considered internal and subject to change: ++ +[%autowidth] +|=== +| old namespace | new internal namespace + +| `etaoin.client` +| `etaoin.impl.client` + +| `etaoin.driver` +| `etaoin.impl.driver` + +| `etaoin.proc` +| `etaoin.impl.proc` + +| `etaoin.util` +| `etaoin.impl.util` + +| `etaoin.xpath` +| `etaoin.impl.xpath` + +| `etaoin.ide.api` +| `etaoin.ide.impl.api` + +| `etaoin.ide.spec` +| `etaoin.ide.impl.spec` + +|=== ++ +The following vars are now considered internal and subject to change: ++ +[%autowidth] +|=== +| namespace | var + +.16+|`etaoin.api` +| `default-locator` +| `dispatch-driver` +| `find-elements*` +| `format-date` +| `get-pwd` +| `join-path` +| `locator-css` +| `locator-xpath` +| `make-url` +| `make-screenshot-file-path` +| `postmortem-handler` +| `process-log` +| `proxy-env` +| `use-locator` +| `with-exception` +| `with-locator` + +.8+| `etaoin.dev` +| `build-request` +| `group-requests` +| `log->request` +| `parse-json` +| `parse-method` +| `process-log` +| `request?` +| `try-parse-int` + +| `etaoin.ide.flow` +| all except for: `run-ide-script` + +| `etaoin.ide.main` +| all except for: `-main` +|=== ++ +If we got this wrong your code will fail, you will tell us, and we can discuss. + +Other Changes + +* https://github.com/clj-commons/etaoin/issues/380[#380]: Etaoin is now Babashka compatible! +* https://github.com/clj-commons/etaoin/issues/413[#413]: Etaoin now exports a clj-kondo config to help with the linting of its many handy macros +* https://github.com/clj-commons/etaoin/issues/383[#383]: Drop testing for Safari on Windows, Apple no longer releases Safari for Windows +* https://github.com/clj-commons/etaoin/issues/388[#388]: Drop testing for PhantomJS, development has long ago stopped for PhantomJS +* https://github.com/clj-commons/etaoin/issues/387[#387]: No longer testing multiple key modifiers for a single webdriver send keys request +* https://github.com/clj-commons/etaoin/issues/384[#384]: Look for `safaridriver` on PATH by default +* https://github.com/clj-commons/etaoin/issues/402[#402]: Only send body for webdriver `POST` requests to appease `safaridriver` +* https://github.com/clj-commons/etaoin/issues/403[#403]: The `select` fn now clicks on the `select` element before clicking the `option` element to appease `safaridriver` +* https://github.com/clj-commons/etaoin/issues/408[#408]: Fix `switch-window` for `msedgedriver` +* https://github.com/clj-commons/etaoin/issues/432[#432]: Switched from `project.clj` to `deps.edn`. +This will allow for easier testing of unreleased versions of Etaoin via git deps. +It also unconvered that our minimum Clojure version was 1.10, instead of the advertised v1.9. +Fixed. +* https://github.com/clj-commons/etaoin/issues/444[#444]: Visibility checks fixed for firefox and chrome (thanks https://github.com/daveyarwood[@daveyarwood]!) +* https://github.com/clj-commons/etaoin/issues/446[#446]: Bump Etaoin dependencies to current releases +* Docs +** Reviewed and updated API docstrings +** https://github.com/clj-commons/etaoin/issues/393[#393]: Add changelog +** https://github.com/clj-commons/etaoin/issues/426[#426]: Reorganize into separate guides + +** https://github.com/clj-commons/etaoin/issues/396[#396]: Move from Markdown to AsciiDoc +** User guide +*** Reviewed, re-organized, hopefully clarified some things +*** Checking code blocks with https://github.com/lread/test-doc-blocks[test-doc-blocks] +*** https://github.com/clj-commons/etaoin/commit/f3f0370fb76bc353c14293243410db1641f99c70[f3f0370]: A new troubleshooting tip (thanks https://github.com/jkrasnay[@jkrasnay]!) +*** https://github.com/clj-commons/etaoin/pull/364[#364]: A new usage example (thanks https://github.com/kidd[kidd]!) +* Internal quality +** https://github.com/clj-commons/etaoin/issues/382[#382]: Fix process fork testing on Windows +** https://github.com/clj-commons/etaoin/issues/391[#391]: Identify browser name on failed ide tests +** https://github.com/clj-commons/etaoin/issues/390[#390]: Add internal clj-kondo config +** https://github.com/clj-commons/etaoin/issues/381[#381]: In addition to ubuntu, now also testing on macOS and Windows (using GitHub Actions https://github.com/clj-commons/etaoin/issues/392[#392] with parallelization https://github.com/clj-commons/etaoin/issues/420[#420]) + +== v0.4.6 + +* https://github.com/clj-commons/etaoin/issues/367[#367]: Use actions to fix double-click + +== v0.4.5 + +* https://github.com/clj-commons/etaoin/pull/365[#365]: Improve status messages when waiting + +== v0.4.4 + +* Add v2 api + +== v0.4.3 + +* Fix user-agent functions + +== v0.4.2 + +* Add user-agent functions to api + +== v0.4.1 + +* https://github.com/clj-commons/etaoin/issues/347[#347]: Add support for mltiple clicks +* https://github.com/clj-commons/etaoin/pull/349[#349]: Fix typo for duration within `add-action` + +== v0.4.0 [breaking] + +Breaking Changes + +* https://github.com/clj-commons/etaoin/issues/111[#111]: the driver instance is *now a map instead of an atom*. ++ +Breaking changes are never embarked on lightly but we felt it important to transition to more a more idiomatic use of Clojure. ++ +All the internal functions that used to modify the driver atom now just return a new version of a map. +If you have `swap!` or something similar in your code for the driver, please refactor your code before you update. + +Other Changes + +* https://github.com/clj-commons/etaoin/issues/44[#44]: Add `with-screenshots` macro +* https://github.com/clj-commons/etaoin/issues/196[#196]: Add support for running Selenium IDE files +** https://github.com/clj-commons/etaoin/issues/336[#336]: Docs +** https://github.com/clj-commons/etaoin/issues/329[#329]: Cli entry point +* https://github.com/clj-commons/etaoin/issues/103[#103]: Add w3c webdriver action support + +== v0.3.10 + +* https://github.com/clj-commons/etaoin/issues/317[#317]: Add a description of the trouble with chromedriver +* https://github.com/clj-commons/etaoin/issues/316[#316]: Add with-tmp-dir for tests +* https://github.com/clj-commons/etaoin/issues/307[#307]: update examples for docker and remote connection +* https://github.com/clj-commons/etaoin/issues/315[#315]: Add :fn/has-string && return old version has-text +* https://github.com/clj-commons/etaoin/issues/314[#314]: Fix quit fn for remote driver +* https://github.com/clj-commons/etaoin/issues/311[#311]: Fix typos - headless +* https://github.com/clj-commons/etaoin/issues/309[#309]: Add test prevent process fork +* https://github.com/clj-commons/etaoin/issues/308[#308]: Fix check $HOST +* https://github.com/clj-commons/etaoin/issues/306[#306]: Make service functions private +* https://github.com/clj-commons/etaoin/issues/304[#304]: Add default port for remote connection +* https://github.com/clj-commons/etaoin/issues/303[#303]: Refactoring remote connection capabilities +* https://github.com/clj-commons/etaoin/issues/302[#302]: Add fns for connecting to remote drivers +* https://github.com/clj-commons/etaoin/issues/300[#300]: Add x11 for tests +* https://github.com/clj-commons/etaoin/issues/298[#298]: Make tests run in display mode +* bump version in readme +* Version 0.3.10-SNAPSHOT + +== v0.3.9 + +* Fix test ignorance +* https://github.com/clj-commons/etaoin/issues/297[#297]: Freeze chrome profile test +* https://github.com/clj-commons/etaoin/issues/293[#293]: Fix chrome profile test && reflect warn +* https://github.com/clj-commons/etaoin/issues/292[#292]: Add a description of the trouble +* https://github.com/clj-commons/etaoin/issues/291[#291]: Add log level for driver +* https://github.com/clj-commons/etaoin/issues/289[#289]: Fix chrome profile +* https://github.com/clj-commons/etaoin/issues/288[#288]: Fix get-performance-logs +* https://github.com/clj-commons/etaoin/issues/287[#287]: Fix tests +* https://github.com/clj-commons/etaoin/issues/286[#286]: Fix example for docker +* https://github.com/clj-commons/etaoin/issues/285[#285]: Add example for query-all +* https://github.com/clj-commons/etaoin/issues/284[#284]: Fix :fn/text && :fn/has-text +* https://github.com/clj-commons/etaoin/issues/281[#281]: Move session opts from run- to connect- driver +* https://github.com/clj-commons/etaoin/issues/279[#279]: Add click on field to fill-human +* https://github.com/clj-commons/etaoin/issues/277[#277]: Add fill human multi +* https://github.com/clj-commons/etaoin/issues/276[#276]: Add :fn/link +* https://github.com/clj-commons/etaoin/issues/275[#275]: Decrease default timeout +* https://github.com/clj-commons/etaoin/issues/274[#274]: Add shortcut wait-has-text-everywhere +* https://github.com/clj-commons/etaoin/issues/273[#273]: Add examples of query functions +* https://github.com/clj-commons/etaoin/issues/271[#271]: Add wrap-default-timeout && wrap-default-interval +* https://github.com/clj-commons/etaoin/issues/270[#270]: Add fn select decription && some fix +* https://github.com/clj-commons/etaoin/issues/269[#269]: Add query-tree fn +* https://github.com/clj-commons/etaoin/issues/268[#268]: Make some declarations private for :use +* Fix creating dirs in postmortem handler +* https://github.com/clj-commons/etaoin/issues/267[#267]: Fix creating dirs in postmortem handler +* https://github.com/clj-commons/etaoin/issues/266[#266]: Fix get-inner-html for phantomjs +* https://github.com/clj-commons/etaoin/issues/265[#265]: Add phantomjs to docker && circleci +* https://github.com/clj-commons/etaoin/issues/264[#264]: Fix switch-window with PhantomJS +* https://github.com/clj-commons/etaoin/issues/262[#262]: Add pre checks for query fns +* https://github.com/clj-commons/etaoin/issues/263[#263]: Fix test-cookies for newest firefox + +== v0.3.8 + +* https://github.com/clj-commons/etaoin/issues/261[#261]: Fix geckodriver install +* https://github.com/clj-commons/etaoin/issues/259[#259]: Add setting of env +* https://github.com/clj-commons/etaoin/issues/258[#258]: Fix installation geckodriver in docker +* https://github.com/clj-commons/etaoin/issues/257[#257]: Add logging to files +* https://github.com/clj-commons/etaoin/issues/256[#256]: Add opts for human input +* https://github.com/clj-commons/etaoin/issues/255[#255]: Add driver installation check +* https://github.com/clj-commons/etaoin/issues/254[#254]: Add http proxy settings +* https://github.com/clj-commons/etaoin/issues/253[#253]: Add edge support +* https://github.com/clj-commons/etaoin/issues/252[#252]: Add docker examle +* https://github.com/clj-commons/etaoin/issues/251[#251]: Add test examples +* https://github.com/clj-commons/etaoin/issues/248[#248]: Add fixture example +* https://github.com/clj-commons/etaoin/issues/249[#249]: Fix headless? && save capabilities +* https://github.com/clj-commons/etaoin/issues/247[#247]: Fix reflection warning && indent project.clj +* https://github.com/clj-commons/etaoin/issues/246[#246]: Aggressive indent +* https://github.com/clj-commons/etaoin/issues/245[#245]: Better free port discovery +* https://github.com/clj-commons/etaoin/issues/244[#244]: Auto release +* Readme updated +* https://github.com/clj-commons/etaoin/issues/241[#241]: Add select fn +* https://github.com/clj-commons/etaoin/issues/238[#238]: Better legacy code +* https://github.com/clj-commons/etaoin/issues/237[#237]: Bump clj-http && cheshire +* https://github.com/clj-commons/etaoin/issues/236[#236]: Add example remote connection +* Add Dockerfile && fix test +* https://github.com/clj-commons/etaoin/issues/233[#233]: Fix tests & circleci config +* https://github.com/clj-commons/etaoin/issues/231[#231]: Move safari from jwp to w3c protocol +* https://github.com/clj-commons/etaoin/issues/223[#223]: Use dir-img and dir-log if passed +* https://github.com/clj-commons/etaoin/issues/230[#230]: Move driver installation part and typo fix +* https://github.com/clj-commons/etaoin/issues/228[#228]: Fix syntax error in docs +* Switch to codox for generating docs + +== v0.3.7 + +* https://github.com/clj-commons/etaoin/issues/242[#242]: Add release tasks +* Readme updated +* https://github.com/clj-commons/etaoin/issues/241[#241]: Add select fn +* https://github.com/clj-commons/etaoin/issues/238[#238]: Better legacy code +* https://github.com/clj-commons/etaoin/issues/237[#237]: Bump clj-http && cheshire +* https://github.com/clj-commons/etaoin/issues/236[#236]: Add example remote connection +* Add Dockerfile && fix test +* https://github.com/clj-commons/etaoin/issues/233[#233]: Fix tests & circleci config +* https://github.com/clj-commons/etaoin/issues/231[#231]: Move safari from jwp to w3c protocol +* https://github.com/clj-commons/etaoin/issues/223[#223]: Use dir-img and dir-log if passed +* https://github.com/clj-commons/etaoin/issues/230[#230]: Move driver installation part and typo fix +* https://github.com/clj-commons/etaoin/issues/228[#228]: Fix syntax error in docs +* Switch to codox for generating docs diff --git a/CNAME b/CNAME deleted file mode 100644 index 6528185c..00000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -etaoin.grishaev.me diff --git a/Dockerfile b/Dockerfile index 8ead7579..044804fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,65 +1,26 @@ -FROM clojure:lein-2.9.3 +FROM eclipse-temurin:8 RUN apt-get -yqq update && \ apt-get -yqq upgrade && \ + apt-get -yqq install libnss3 && \ + apt-get -yqq install bzip2 && \ + apt-get -yqq install imagemagick && \ apt-get -yqq install gnupg2 && \ - apt-get -yqq install xvfb curl unzip && \ + apt-get -yqq install git xvfb curl unzip && \ apt-get -yqq install fonts-ipafont-gothic xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic && \ apt-get install -y ca-certificates jq libfontconfig libgconf-2-4 && \ rm -rf /var/lib/apt/lists/* -# Install Chrome WebDriver -RUN CHROMEDRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` && \ - mkdir -p /opt/chromedriver-$CHROMEDRIVER_VERSION && \ - curl -sS -o /tmp/chromedriver_linux64.zip http://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip && \ - unzip -qq /tmp/chromedriver_linux64.zip -d /opt/chromedriver-$CHROMEDRIVER_VERSION && \ - rm /tmp/chromedriver_linux64.zip && \ - chmod +x /opt/chromedriver-$CHROMEDRIVER_VERSION/chromedriver && \ - ln -fs /opt/chromedriver-$CHROMEDRIVER_VERSION/chromedriver /usr/local/bin/chromedriver +# This dockerfile is not amenable to layer updates because nothing changes +# build with --no-cache -# Install Google Chrome -RUN curl -sS -o - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ - echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list && \ - apt-get -yqq update && \ - apt-get -yqq install google-chrome-stable && \ - rm -rf /var/lib/apt/lists/* - -# Install Firefox -# RUN set -o nounset && set -o errexit && set -o xtrace -RUN FIREFOX_URI="https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US" && \ - wget --quiet --content-disposition "$FIREFOX_URI" && \ - tar xf firefox-*.tar.bz2 -C /usr/local && \ - ln -s /usr/local/firefox/firefox /usr/local/bin/firefox - -# Install Geckodriver -RUN GECKODRIVER_META="https://api.github.com/repos/mozilla/geckodriver/releases/latest" && \ - GECKODRIVER_LATEST_RELEASE_URL=$(curl $GECKODRIVER_META | jq -r ".assets[] | select(.content_type==\"application/gzip\") | select(.name | test(\"linux64\")) | .browser_download_url") && \ - curl --silent --show-error --location --fail --retry 3 --output /tmp/geckodriver_linux64.tar.gz "$GECKODRIVER_LATEST_RELEASE_URL" && \ - cd /tmp && \ - tar xf geckodriver_linux64.tar.gz && \ - chmod +x geckodriver && \ - mv geckodriver /usr/local/bin/ - -# Install PhantomJS -RUN PHANTOMJS_VERSION=phantomjs-2.1.1-linux-x86_64 && \ - wget --quiet --content-disposition https://bitbucket.org/ariya/phantomjs/downloads/$PHANTOMJS_VERSION.tar.bz2 && \ - tar xf $PHANTOMJS_VERSION.tar.bz2 -C /usr/local && \ - ln -s /usr/local/$PHANTOMJS_VERSION/bin/phantomjs /usr/local/bin/phantomjs && \ - rm $PHANTOMJS_VERSION.tar.bz2 - -# PhantomJS requires an OpenSSL config even if it's an empty one, -# else it'll complain about "libssl_conf.so: cannot open shared object file" -# which seems to be a recent bug. -ENV OPENSSL_CONF=/opt/openssl.cnf +# Install babashka, we use it for (and after) docker image building +RUN curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install && \ + chmod +x install && \ + ./install && \ + rm ./install +# Copy over etaoin only for its build support, delete after use COPY ./ /etaoin -RUN cd /etaoin && lein deps && rm -rf /etaoin - -ENV DISPLAY :99 -RUN printf '#!/bin/sh\nXvfb :99 -screen 0 1280x1024x24 &\nexec "$@"\n' > /tmp/entrypoint \ - && chmod +x /tmp/entrypoint \ - && mv /tmp/entrypoint /entrypoint.sh - -ENV ETAOIN_TEST_DRIVERS="[:firefox :chrome :phantom]" -ENTRYPOINT ["/entrypoint.sh"] +RUN cd /etaoin && bb -docker-install && bb download-deps && rm -rf /etaoin diff --git a/Makefile b/Makefile deleted file mode 100644 index e6fda3e8..00000000 --- a/Makefile +++ /dev/null @@ -1,77 +0,0 @@ - -repl: - lein repl - -repl-1.9: - lein with-profile +1.9 repl - -.PHONY: test -test: - lein test - -orig: - find . -name '*.orig' -delete - -.PHONY: tags -tags: - ctags -e -R ./src - -deploy: - lein deploy clojars - -.PHONY: release -release: - lein release - -toc-install: - npm install --save markdown-toc - -toc-build: - node_modules/.bin/markdown-toc -i README.md - -.PHONY: kill -kill: - pkill chromedriver || true - pkill geckodriver || true - pkill safaridriver || true - pkill phantomjs || true - - -IMAGE := etaoin - -.PHONY: docker-build -docker-build: - docker build --no-cache -t ${IMAGE}:latest . - -.PHONY: check-host -check-host: - ifndef HOST - $(error The HOST variable is not set, please do `export HOST=$$HOST` first) - endif - -# works only on mac + quartz -.PHONY: docker-test-display -docker-test-display: check-host - xhost + - docker run --rm \ - -v ${CURDIR}:/etaoin \ - -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=$(HOST):0 \ - -w /etaoin ${IMAGE}:latest \ - lein test || \ - xhost - - -.PHONY: docker-test -docker-test: - docker run --rm \ - -v ${CURDIR}:/etaoin \ - -w /etaoin ${IMAGE}:latest \ - lein test - - -gh-init: - git clone -b gh-pages --single-branch git@github.com:igrishaev/etaoin.git gh-pages - - -gh-build: - lein codox - cd gh-pages && git add -A && git commit -m "docs updated" && git push diff --git a/README.adoc b/README.adoc new file mode 100644 index 00000000..8631651c --- /dev/null +++ b/README.adoc @@ -0,0 +1,105 @@ += Etaoin +:toclevels: 4 +:project-src-coords: clj-commons/etaoin +:project-mvn-coords: etaoin/etaoin +:url-webdriver: https://www.w3.org/TR/webdriver/ +:url-wiki: https://en.wikipedia.org/wiki/Etaoin_shrdlu#Literature +:url-doc: https://cljdoc.org/d/{project-mvn-coords} +:url-slack: https://clojurians.slack.com/messages/C7KDM0EKW/ + +// Badges +link:{url-doc}[image:https://cljdoc.org/badge/{project-mvn-coords}[cljdoc]] +https://github.com/{project-src-coords}/actions/workflows/test.yml[image:https://github.com/{project-src-coords}/actions/workflows/test.yml/badge.svg[GitHub Actions Tests]] +https://clojars.org/{project-mvn-coords}[image:https://img.shields.io/clojars/v/{project-mvn-coords}.svg[Clojars project]] +https://babashka.org[image:https://raw.githubusercontent.com/babashka/babashka/master/logo/badge.svg[bb compatible]] +https://clojurians.slack.com/archives/C7KDM0EKW[image:https://img.shields.io/badge/slack-join_chat-brightgreen.svg[Join chat]] + +A pure Clojure implementation of the link:{url-webdriver}[Webdriver] protocol, named after link:{url-wiki}[Etaoin Shrdlu] -- a typing machine that came to life after a mysterious note was produced on it. + +Use the Etaoin library to automate a browser, test your frontend behaviour, simulate human actions or whatever you want. + +== Benefits + +* Selenium-free: no big dependencies, no tons of downloaded jars, etc. +* Lightweight, fast. +Simple, easy to understand. +* Compact: just one main namespace with a couple of helpers. +* Declarative: the code is just a list of actions. + +== Capabilities + +* Currently supports Chrome, Firefox, Safari and Edge. +* Can either connect to a remote WebDriver process, or have Etaoin launch one for you. +* Run your unit tests directly from Emacs by pressing `C-t t` as usual. +* Can imitate human-like behaviour (delays, typos, etc). + +== Documentation + +API docs and articles are best viewed on link:{url-doc}[cljdoc]: + +* link:doc/01-user-guide.adoc[User Guide] +* link:doc/02-developer-guide.adoc[Developer Guide] + +See also: + +:ui-test: http://grishaev.me/en/ui-test +:stream: https://www.youtube.com/watch?v=cLL_5rETLWY + +* link:{url-slack}[Slack channel] +* link:{ui-test}[Thoughts on UI tests] +Ivan's blog-post about pitfalls that can occur when testing UI. +* link:{stream}[Live-coding session] where Ivan works through some Etaoin issues. + +== Who uses Etaoin? + +* https://www.exoscale.com/[Exoscale] +* https://www.flyerbee.com/[Flyerbee] +* http://www.barrick.com/[Barrick Gold] +* http://drevidence.com/[Doctor Evidence] +* https://kevel.com/[Kevel (formerly Adzerk)] +* https://www.rate.com/[Guaranteed Rate] + +You are most welcome to submit your company or project to this list. + +== Versioning + +Eatoin uses: `major`.`minor`.`patch`-`test-qualifier` + +* `major` increments when a non alpha release API has been broken - something, as a rule, we'd like to avoid. +* `minor` increments to convey significant new features have been added. +* `patch` indicates bug fixes or minor changes - it is the total number of releases to date. +* `test-qualifier` is absent for stable releases. +Can be `alpha`, `beta`, `rc1`, etc. + +== People + +=== Contributors + +* https://github.com/Uunnamed[Alexey Shipilov] +* https://github.com/AdamFrey[Adam Frey] +* https://github.com/jwkoelewijn[JW Koelewijn] +* https://github.com/nenadalm[Miloslav Nenadál] +* https://github.com/atsman[Aleh Atsman] +* https://github.com/marco-m[Marco Molteni] +* https://github.com/nebesnytihohod[Maxim Stasenkov] +* https://github.com/daveyarwood[Dave Yarwood] +* https://github.com/jkrasnay[John Krasnay] +* https://github.com/kidd[Raimon Grau] + +=== Current Maintainers + +* https://github.com/lread[Lee Read] +* https://github.com/borkdude[Michiel Borkent] + +=== Founder + +* https://github.com/igrishaev[Ivan Grishaev] + +Etaoin is open for your improvements and ideas. +If any of unit tests fail on your machine, please submit an issue giving your OS version, browser and console output. + +== License + +Copyright © 2017—2020 Ivan Grishaev. + +Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version. diff --git a/README.md b/README.md deleted file mode 100644 index a63de204..00000000 --- a/README.md +++ /dev/null @@ -1,2123 +0,0 @@ -[url-webdriver]: https://www.w3.org/TR/webdriver/ -[url-wiki]: https://en.wikipedia.org/wiki/Etaoin_shrdlu#Literature -[url-tests]: https://github.com/igrishaev/etaoin/blob/master/test/etaoin/api_test.clj -[url-doc]: http://etaoin.grishaev.me/ -[url-slack]: https://clojurians.slack.com/messages/C7KDM0EKW/ - -# Etaoin - -[![CircleCI](https://circleci.com/gh/igrishaev/etaoin.svg?style=svg)](https://circleci.com/gh/igrishaev/etaoin) - -Pure Clojure implementation of [Webdriver][url-webdriver] protocol. Use that -library to automate a browser, test your frontend behaviour, simulate human -actions or whatever you want. - -It's named after [Etaoin Shrdlu][url-wiki] -- a typing machine that became alive -after a mysteries note was produced on it. - -## Release Notes - -### Atom turns into a map - -Since `0.4.0`, the driver instance is **a map but not an atom** like -it used to be. It was a difficult solution to decide on, yet we've got rid of -atom to follow Clojure way in our code. Generally speaking, you never deref a -driver or store something inside it. All the internal functions that used to -modify the instance now just return a new version of a map. If you have `swap!` -or something similar in your code for the driver, please refactor your code -before you update. - -[actions]: https://www.w3.org/TR/webdriver/#actions - -### Actions - -Since `0.4.0`, the library supports [Webdriver -Actions][actions]. Actions are commands sent to the driver in batch. See the -detailed related section in ToC. - -### Selenium IDE support - -[ide]: https://www.selenium.dev/selenium-ide/ - -Since `0.4.0`, Etaoin can play script files created in the interactive -[Selenium IDE][ide]. See the related section below. - -# Table of Contents - - - -- [Benefits](#benefits) -- [Capabilities](#capabilities) -- [Who uses it?](#who-uses-it) -- [Documentation](#documentation) -- [Installation](#installation) - * [Installing the etaoin library](#installing-the-etaoin-library) - * [Installing the Browser Drivers](#installing-the-browser-drivers) -- [Getting started](#getting-started) -- [Querying elements](#querying-elements) - * [Simple queries, XPath, CSS](#simple-queries-xpath-css) - * [Map syntax for querying](#map-syntax-for-querying) - * [Vector syntax for querying](#vector-syntax-for-querying) - * [Advanced queries](#advanced-queries) - + [Querying the *nth* element matched](#querying-the-nth-element-matched) - + [Getting elements like in a tree](#getting-elements-like-in-a-tree) - * [Interacting with queried elements](#interacting-with-queried-elements) -- [Emulation of human input](#emulation-of-human-input) -- [Mouse clicks](#mouse-clicks) -- [Actions](#actions) -- [File uploading](#file-uploading) -- [Screenshots](#screenshots) - * [Screening elements](#screening-elements) - * [Screening after each form](#screening-after-each-form) -- [Using headless drivers](#using-headless-drivers) -- [Connection to remote webdriver](#connection-to-remote-webdriver) -- [Webdriver in Docker](#webdriver-in-docker) -- [HTTP Proxy](#http-proxy) -- [Devtools: tracking HTTP requests, XHR (Ajax)](#devtools-tracking-http-requests-xhr-ajax) -- [Postmortem: auto-save artifacts in case of exception](#postmortem-auto-save-artifacts-in-case-of-exception) -- [Reading browser's logs](#reading-browsers-logs) -- [Additional parameters](#additional-parameters) -- [Eager page load](#eager-page-load) -- [Keyboard chords](#keyboard-chords) -- [File download directory](#file-download-directory) -- [Setting browser profile](#setting-browser-profile) - * [Create and find a profile in Chrome](#create-and-find-a-profile-in-chrome) - * [Create and find a profile in Firefox](#create-and-find-a-profile-in-firefox) - * [Running a driver with a profile](#running-a-driver-with-a-profile) -- [Scrolling](#scrolling) -- [Working with frames and iframes](#working-with-frames-and-iframes) -- [Executing Javascript](#executing-javascript) - * [Asynchronous scripts](#asynchronous-scripts) -- [Wait functions](#wait-functions) -- [Writing Integration Tests For Your Application](#writing-integration-tests-for-your-application) - * [Basic fixture](#basic-fixture) - * [Multi-Driver Fixtures](#multi-driver-fixtures) - * [Postmortem Handler To Collect Artifacts](#postmortem-handler-to-collect-artifacts) - * [Running Tests By Tag](#running-tests-by-tag) - * [Check whether a file has been downloaded](#check-whether-a-file-has-been-downloaded) -- [Running IDE files (new!)](#running-ide-files-new) - * [CLI arguments](#cli-arguments) -- [Troubleshooting](#troubleshooting) - * [Calling maximize function throws an error](#calling-maximize-function-throws-an-error) - * [Querying wrong elements with XPath expressions](#querying-wrong-elements-with-xpath-expressions) - * [Clicking On Non-Visible Element](#clicking-on-non-visible-element) - * [Unpredictable errors in Chrome when window is not active](#unpredictable-errors-in-chrome-when-window-is-not-active) - * [Invalid argument: can't kill an exited process](#invalid-argument-cant-kill-an-exited-process) - * [DevToolsActivePort file doesn't exist](#devtoolsactiveport-file-doesnt-exist) -- [Contributors](#contributors) -- [Other materials](#other-materials) -- [License](#license) - - - -## Benefits - -- Selenium-free: no long dependencies, no tons of downloaded jars, etc. -- Lightweight, fast. Simple, easy to understand. -- Compact: just one main module with a couple of helpers. -- Declarative: the code is just a list of actions. - -## Capabilities - -- Currently supports Chrome, Firefox, Phantom.js and Safari (partially). -- May either connect to a remote driver or run it on your local machine. -- Run your unit tests directly from Emacs pressing `C-t t` as usual. -- Can imitate human-like behaviour (delays, typos, etc). - -## Who uses it? - -- [Exoscale](https://www.exoscale.com/) -- [Flyerbee](https://www.flyerbee.com/) -- [Roomkey](https://www.roomkey.com/) -- [Barrick Gold](http://www.barrick.com/) -- [Doctor Evidence](http://drevidence.com/) -- [Adzerk](https://adzerk.com/) - -You are welcome to submit your company into that list. - -## Documentation - -- [API docs][url-doc] -- [Slack channel][url-slack] -- [Unit tests][url-tests] - -## Installation - -There are two steps to installation: - 1. Install the library `etaoin` into your clojure code - 2. Install the drivers for each browser - -### Installing the etaoin library -Add the following into `:dependencies` vector in your `project.clj` file: - -``` -[etaoin "0.4.1"] -``` - -Works with Clojure 1.9 and above. - -### Installing the Browser Drivers - -[url-webdriver]: https://www.w3.org/TR/webdriver/ -[url-tests]: https://github.com/igrishaev/etaoin/blob/master/test/etaoin/api_test.clj -[url-chromedriver]: https://sites.google.com/a/chromium.org/chromedriver/ -[url-chromedriver-dl]: https://sites.google.com/a/chromium.org/chromedriver/downloads -[url-geckodriver-dl]: https://github.com/mozilla/geckodriver/releases -[url-phantom-dl]: http://phantomjs.org/download.html -[url-webkit]: https://webkit.org/blog/6900/webdriver-support-in-safari-10/ - -This page provides instructions on how to install drivers you need to automate -your browser. - -Install Chrome and Firefox browsers downloading them from the official -sites. There won't be a problem on all the platforms. - -Install specific drivers you need: - -- Google [Chrome driver][url-chromedriver]: - - - `brew cask install chromedriver` for Mac users - - or download compiled binaries from the [official site][url-chromedriver-dl]. - - ensure you have at least `2.28` version installed. `2.27` and below has a - bug related to maximizing a window (see [[Troubleshooting]]). - -- Geckodriver, a driver for Firefox: - - - `brew install geckodriver` for Mac users - - or download it from the official [Mozilla site][url-geckodriver-dl]. - -- Phantom.js browser: - - - `brew install phantomjs` For Mac users - - or download it from the [official site][url-phantom-dl]. - -- Safari Driver (for Mac only): - - - update your Mac OS to El Captain using App Store; - - set up Safari options as the [Webkit page][url-webkit] says (scroll down to - "Running the Example in Safari" section). - -Now, check your installation launching any of these commands. For each command, -an endless process with a local HTTP server should start. - -```bash -chromedriver -geckodriver -phantomjs --wd -safaridriver -p 0 -``` - -You may run tests for this library launching: - -```bash -lein test -``` - -You'll see browser windows open and close in series. The tests use a local HTML -file with a special layout to validate the most of the cases. - -See below for the [Troubleshooting section](https://github.com/igrishaev/etaoin#troubleshooting) if you have problems - -## Getting started - -The good news you may automate your browser directly from the REPL: - -```clojure -(use 'etaoin.api) -(require '[etaoin.keys :as k]) - -(def driver (firefox)) ;; here, a Firefox window should appear - -;; let's perform a quick Wiki session -(go driver "https://en.wikipedia.org/") -(wait-visible driver [{:id :simpleSearch} {:tag :input :name :search}]) - -;; search for something -(fill driver {:tag :input :name :search} "Clojure programming language") -(fill driver {:tag :input :name :search} k/enter) -(wait-visible driver {:class :mw-search-results}) - -;; select an `option` in select-box by visible text -;; -(select driver :country "France") -(get-element-value driver :country) -;;=> "fr" - -;; I'm sure the first link is what I was looking for -(click driver [{:class :mw-search-results} {:class :mw-search-result-heading} {:tag :a}]) -(wait-visible driver {:id :firstHeading}) - -;; let's ensure -(get-url driver) ;; "https://en.wikipedia.org/wiki/Clojure" - -(get-title driver) ;; "Clojure - Wikipedia" - -(has-text? driver "Clojure") ;; true - -;; navigate on history -(back driver) -(forward driver) -(refresh driver) -(get-title driver) ;; "Clojure - Wikipedia" - -;; stops Firefox and HTTP server -(quit driver) -``` - -You see, any function requires a driver instance as the first argument. So you -may simplify it using `doto` macros: - -```clojure -(def driver (firefox)) -(doto driver - (go "https://en.wikipedia.org/") - (wait-visible [{:id :simpleSearch} {:tag :input :name :search}]) - ;; ... - (fill {:tag :input :name :search} k/enter) - (wait-visible {:class :mw-search-results}) - (click :some-button) - ;; ... - (wait-visible {:id :firstHeading}) - ;; ... - (quit)) -``` - -In that case, your code looks like a DSL designed just for such purposes. - -You can use `fill-multi` to shorten the code like: - -``` clojure -(fill driver :login "login") -(fill driver :password "pass") -(fill driver :textarea "some text") -``` - -into - -``` clojure -(fill-multi driver {:login "login" - :password "pass" - :textarea "some text"}) -``` - -If any exception occurs during a browser session, the external process might -hang forever until you kill it manually. To prevent it, use `with-` -macros as follows: - -```clojure -(with-firefox {} ff ;; additional options first, then bind name - (doto ff - (go "https://google.com") - ...)) -``` - -Whatever happens during a session, the process will be stopped anyway. - -## Querying elements - -Most of the functions like `click`, `fill`, etc require a query term to discover -an element on a page. For example: - -```clojure -(click driver {:tag :button}) -(fill driver {:id "searchInput"} "Clojure") -``` - -[xpath-sel]:https://www.w3schools.com/xml/xpath_syntax.asp -[css-sel]:https://www.w3schools.com/cssref/css_selectors.asp - -The library supports the following query types and values. - -### Simple queries, XPath, CSS - -- `:active` stands for the current active element. When opening Google page for - example, it focuses the cursor on the main search input. So there is no need - to click on in manually. Example: - - ```clojure - (fill driver :active "Let's search for something" keys/enter) - ``` - -- any other keyword that indicates an element's ID. For Google page, it is - `:lst-ib` or `"lst-ib"` (strings are also supported). The registry - matters. Example: - - ```clojure - (fill driver :lst-ib "What is the Matrix?" keys/enter) - ``` -- a string with an [XPath][xpath-sel] expression. Be careful when writing them - manually. Check the `Troubleshooting` section below. Example: - - ```clojure - (fill driver ".//input[@id='lst-ib'][@name='q']" "XPath in action!" keys/enter) - ``` - -- a map with either `:xpath` or `:css` key with a string expression of - corresponding syntax. Example: - - ```clojure - (fill driver {:xpath ".//input[@id='lst-ib']"} "XPath selector" keys/enter) - (fill driver {:css "input#lst-ib[name='q']"} "CSS selector" keys/enter) - ``` - - See the [CSS selector][css-sel] manual for more info. - - -### Map syntax for querying - -A query might be any other map that represents an XPath expression as data. The -rules are: - -- A `:tag` key represents a tag's name. It becomes `*` when not passed. -- An `:index` key expands into the trailing `[x]` clause. Useful when you need - to select a third row from a table for example. -- Any non-special key represents an attribute and its value. -- A special key has `:fn/` namespace and expands into something specific. - -Examples: - -- find the first `div` tag - ```clojure - (query driver {:tag :div}) - ;; expands into .//div - ``` - -- find the n-th `div` tag - ```clojure - (query driver {:tag :div :index 1}) - ;; expands into .//div[1] - ``` - -- find the tag `a` with the class attribute equals to `active` - -``` clojure - (query driver {:tag :a :class "active"}) - ;; ".//a[@class=\"active\"]" -``` - -- find a form by its attributes: - - ```clojure - (query driver {:tag :form :method :GET :class :message}) - ;; expands into .//form[@method="GET"][@class="message"] - ``` - -- find a button by its text (exact match): - - ```clojure - (query driver {:tag :button :fn/text "Press Me"}) - ;; .//button[text()="Press Me"] - ``` - -- find an nth element (`p`, `a`, whatever) with "download" text: - - ```clojure - (query driver {:fn/has-text "download" :index 3}) - ;; .//*[contains(text(), "download")][3] - ``` - -- find an element that has the following class: - - ```clojure - (query driver {:tag :div :fn/has-class "overlay"}) - ;; .//div[contains(@class, "overlay")] - ``` - -- find an element that has the following domain in a href: - - ```clojure - (query driver {:tag :a :fn/link "google.com"}) - ;; .//a[contains(@href, "google.com")] - ``` - -- find an element that has the following classes at once: - - ```clojure - (query driver {:fn/has-classes [:active :sticky :marked]}) - ;; .//*[contains(@class, "active")][contains(@class, "sticky")][contains(@class, "marked")] - ``` - -- find the enabled/disabled input widgets: - - ```clojure - ;; first input - (query driver {:tag :input :fn/disabled true}) - ;; .//input[@disabled=true()] - (query driver {:tag :input :fn/enabled true}) - ;; .//input[@enabled=true()] - - ;; all inputs - (query-all driver {:tag :input :fn/disabled true}) - ;; .//input[@disabled=true()] - ``` - -### Vector syntax for querying - -A query might be a vector that consists from any expressions mentioned above. In -such a query, every next term searches from a previous one recursively. - -A simple example: - -```clojure -(click driver [{:tag :html} {:tag :body} {:tag :a}]) -``` - -You may combine both XPath and CSS expressions as well (pay attention at a -leading dot in XPath expression: - -```clojure -(click driver [{:tag :html} {:css "div.class"} ".//a[@class='download']"]) -``` - -### Advanced queries - -#### Querying the *nth* element matched - -Sometimes you may need to interact with the *nth* element of a query, for -instance when wanting to click on the second link in this example: - -```html - -``` - -In this case you may either use the `:index` directive that is supported for -XPath expressions like this: - -```clojure -(click driver [{:tag :li :class :search-result :index 2} {:tag :a}]) -``` - -[nth-child]: https://www.w3schools.com/CSSref/sel_nth-child.asp - -or you can use the [nth-child trick][nth-child] with the CSS expression like -this: - -```clojure -(click driver {:css "li.search-result:nth-child(2) a"}) -``` - -Finally it is also possible to obtain the *nth* element directly by using -`query-all`: - -```clojure -(click-el driver (nth (query-all driver {:css "li.search-result a"}) 2)) -``` - -Note the use of `click-el` here, as `query-all` returns an element, not a -selector that can be passed to `click` directly. - -#### Getting elements like in a tree - -`query-tree` takes selectors and acts like a tree. -Every next selector queries elements from the previous ones. -The fist selector relies on find-elements, and the rest ones use find-elements-from - - ```clojure - (query-tree driver {:tag :div} {:tag :a}) - ``` - - means - - ``` - {:tag :div} -> [div1 div2 div3] - div1 -> [a1 a2 a3] - div2 -> [a4 a5 a6] - div3 -> [a7 a8 a9] - ``` - so the result will be [a1 ... a9] - -### Interacting with queried elements - -To interact with elements found via a query you have to pass the query result to -either `click-el` or `fill-el`: - -```clojure -(click-el driver (first (query-all driver {:tag :a}))) -``` - -So you may collect elements into a vector and arbitrarily interact with them -at any time: - -```clojure -(def elements (query-all driver {:tag :input :type :text}) - -(fill-el driver (first elements) "This is a test") -(fill-el driver (rand-nth elements) "I like tests!") -``` - - -## Emulation of human input - -For the purpose of emulating human input, you can use the `fill-human` function. -The following options are enabled by default: - -``` clojure -{:mistake-prob 0.1 ;; a real number from 0.1 to 0.9, the higher the number, the more typos will be made - :pause-max 0.2} ;; max typing delay in seconds -``` - -and you can redefine them: - -``` clojure -(fill-human driver q text {:mistake-prob 0.5 - :pause-max 1}) - -;; or just use default opts by omitting them -(fill-human driver q text) -``` - -for multiple input with human emulation, use `fill-human-multi` - -``` clojure -(fill-human-multi driver {:login "login" - :pass "password" - :textarea "some text"} - {:mistake-prob 0.5 - :pause-max 1}) -``` - -## Mouse clicks - -The `click` function triggers the left mouse click on an element found by a -query term: - -```clojure -(click driver {:tag :button}) -``` - -The `click` function uses only the first element found by the query, which -sometimes leads to clicking on the wrong items. To ensure there is one and only -one element found, use the `click-single` function. It acts the same but raises -an exception when querying the page returns multiple elements: - -```clojure -(click-single driver {:tag :button :name "search"}) -``` - -A double click is used rarely in web yet is possible with the `double-click` -function (Chrome, Phantom.js): - -```clojure -(double-click driver {:tag :dbl-click-btn}) -``` - -There is also a bunch of "blind" clicking functions. They trigger mouse clicks -on the current mouse position: - -```clojure -(left-click driver) -(middle-click driver) -(right-click driver) -``` - -Another bunch of functions do the same but move the mouse pointer to a specified -element before clicking on them: - -```clojure -(left-click-on driver {:tag :img}) -(middle-click-on driver {:tag :img}) -(right-click-on driver {:tag :img}) -``` - -A middle mouse click is useful when opening a link in a new background tab. The -right click sometimes is used to imitate a context menu in web applications. - -## Actions - -The library supports [Webdriver Actions][actions]. In general, actions are -commands describing virtual input devices. - -``` clojure -{:actions [{:type "key" - :id "some name" - :actions [{:type "keyDown" :value cmd} - {:type "keyDown" :value "a"} - {:type "keyUp" :value "a"} - {:type "keyUp" :value cmd} - {:type "pause" :duration 100}]} - {:type "pointer" - :id "UUID or some name" - :parameters {:pointerType "mouse"} - :actions [{:type "pointerMove" :origin "pointer" :x 396 :y 323} - ;; double click - {:type "pointerDown" :duration 0 :button 0} - {:type "pointerUp" :duration 0 :button 0} - {:type "pointerDown" :duration 0 :button 0} - {:type "pointerUp" :duration 0 :button 0}]}]} -``` - -You can create a map manually and send it to the `perform-actions` method: - -``` clojure -(def keyboard-input {:type "key" - :id "some name" - :actions [{:type "keyDown" :value cmd} - {:type "keyDown" :value "a"} - {:type "keyUp" :value "a"} - {:type "keyUp" :value cmd} - {:type "pause" :duration 100}]}) - -(perform-actions driver keyboard-input) -``` - -or use wrappers. First you need to create a virtual input devices, for example: - -``` clojure -(def keyboard (make-key-input)) -``` - -and then fill it with the necessary actions: - -``` clojure -(-> keyboard - (add-key-down keys/shift-left) - (add-key-down "a") - (add-key-up "a") - (add-key-up keys/shift-left)) -``` - -extended example: - -``` clojure -(let [driver (chrome) - _ (go driver "https://google.com") - search-box (query driver {:name :q}) - mouse (-> (make-mouse-input) - (add-pointer-click-el search-box)) - keyboard (-> (make-key-input) - add-pause - (with-key-down keys/shift-left - (add-key-press "e")) - (add-key-press "t") - (add-key-press "a") - (add-key-press "o") - (add-key-press "i") - (add-key-press "n") - (add-key-press keys/enter))] - (perform-actions driver keyboard mouse) - (quit driver)) -``` - -To clear the state of virtual input devices, release all pressed keys etc, use the `release-actions` method: - -``` clojure -(release-actions driver) -``` - -## File uploading - -Clicking on a file input button opens an OS-specific dialog that you are not -allowed to interact with using WebDriver protocol. Use the `upload-file` -function to attach a local file to a file input widget. The function takes a -selector that points to a file input and either a full path as a string or a -native `java.io.File` instance. The file should exist or you'll get an exception -otherwise. Usage example: - -```clojure -(def driver (chrome)) - -;; open a web page that serves uploaded files -(go driver "http://nervgh.github.io/pages/angular-file-upload/examples/simple/") - -;; bound selector to variable; you may also specify an id, class, etc -(def input {:tag :input :type :file}) - -;; upload an image with the first one file input -(def my-file "/Users/ivan/Downloads/sample.png") -(upload-file driver input my-file) - -;; or pass a native Java object: -(require '[clojure.java.io :as io]) -(def my-file (io/file "/Users/ivan/Downloads/sample.png")) -(upload-file driver input my-file) -``` - -## Screenshots - -Calling a `screenshot` function dumps the current page into a PNG image on your -disk: - -```clojure -(screenshot driver "page.png") ;; relative path -(screenshot driver "/Users/ivan/page.png") ;; absolute path -``` - -A native Java File object is also supported: - -```clojure -;; when imported as `[clojure.java.io :as io]` -(screenshot driver (io/file "test.png")) - -;; native object -(screenshot driver (java.io.File. "test-native.png")) -``` - -### Screening elements - -With Firefox and Chrome, you may capture not the whole page but a single element, -say a div, an input widget or whatever. It doesn't work with other browsers for -now. Example: - -```clojure -(screenshot-element driver {:tag :div :class :smart-widget} "smart_widget.png") -``` - -### Screening after each form - -With macro `with-screenshots`, you can make a screenshot after each form - -``` clojure -(with-screenshots driver "../screenshots" - (fill driver :simple-input "1") - (fill driver :simple-input "2") - (fill driver :simple-input "3")) -``` - -what is equivalent to a record: - -``` clojure -(fill driver :simple-input "1") -(screenshot driver "../screenshots/chrome-...123.png") -(fill driver :simple-input "2") -(screenshot driver "../screenshots/chrome-...124.png") -(fill driver :simple-input "3") -(screenshot driver "../screenshots/chrome-...125.png") -``` - -## Using headless drivers - -Recently, Google Chrome and later Firefox started support a feature named -headless mode. When being headless, none of UI windows occur on the screen, only -the stdout output goes into console. This feature allows you to run integration -tests on servers that do not have graphical output device. - -Ensure your browser supports headless mode by checking if it accepts `--headles` -command line argument when running it from the terminal. Phantom.js driver is -headless by its nature (it has never been developed for rendering UI). - -When starting a driver, pass `:headless` boolean flag to switch into headless -mode. Note, only latest version of Chrome and Firefox are supported. For other -drivers, the flag will be ignored. - -```clojure -(def driver (chrome {:headless true})) ;; runs headless Chrome -``` - -or - -```clojure -(def driver (firefox {:headless true})) ;; runs headless Firefox -``` - -To check of any driver has been run in headless mode, use `headless?` predicate: - -```clojure -(headless? driver) ;; true -``` - -Note, it will always return true for Phantom.js instances. - -There are several shortcuts to run Chrome or Firefox in headless mode by -default: - -```clojure -(def driver (chrome-headless)) - -;; or - -(def driver (firefox-headless {...})) ;; with extra settings - -;; or - -(with-chrome-headless nil driver - (go driver "http://example.com")) - -(with-firefox-headless {...} driver ;; extra settings - (go driver "http://example.com")) -``` - -There are also `when-headless` and `when-not-headless` macroses that allow to -perform a bunch of commands only if a browser is in headless mode or not -respectively: - -```clojure -(with-chrome nil driver - ... - (when-not-headless driver - ... some actions that might be not available in headless mode) - ... common actions for both versions) -``` - -## Connection to remote webdriver - -To connect to a driver already running on a local or remote host, you must specify the `:host` parameter -which might be either a hostname (localhost, some.remote.host.net) or an IP address (127.0.0.1, 183.102.156.31) and the `:port`. -If the port is not specified, the default port is set. - -Example: - -```clojure -;; Chrome -(def driver (chrome {:host "127.0.0.1" :port 9515})) ;; for connection to driver on localhost on port 9515 - -;; Firefox -(def driver (firefox {:host "192.168.1.11"})) ;; the default port for firefox is 4444 -``` - -## Webdriver in Docker - -To work with the driver in Docker, you can take ready-made images: - -Example for [Chrome](https://hub.docker.com/r/robcherry/docker-chromedriver/): - -``` -docker run --name chromedriver -p 9515:4444 -d -e CHROMEDRIVER_WHITELISTED_IPS='' robcherry/docker-chromedriver:latest -``` - -for [Firefox](https://hub.docker.com/r/instrumentisto/geckodriver): - -``` -docker run --name geckodriver -p 4444:4444 -d instrumentisto/geckodriver -``` - -To connect to the driver you just need to specify the `:host` parameter as `localhost` or `127.0.0.1` and the `:port` on which it is running. -If the port is not specified, the default port is set. - -``` clojure -(def driver (chrome-headless {:host "localhost" :port 9515 :args ["--no-sandbox"]})) -(def driver (firefox-headless {:host "localhost"})) ;; will try to connect to port 4444 -``` - -## HTTP Proxy - -To set proxy settings use environment variables `HTTP_PROXY`/`HTTPS_PROXY` or pass a map of the following type: - -``` clojure -{:proxy {:http "some.proxy.com:8080" - :ftp "some.proxy.com:8080" - :ssl "some.proxy.com:8080" - :socks {:host "myproxy:1080" :version 5} - :bypass ["http://this.url" "http://that.url"] - :pac-url "localhost:8888"}} - -;; example -(chrome {:proxy {:http "some.proxy.com:8080" - :ssl "some.proxy.com:8080"}}) -``` -Note: A :pac-url for a [proxy autoconfiguration file](https://en.wikipedia.org/wiki/Proxy_auto-config#The_PAC_File). -Used with Safari as the other proxy options do not work in that browser. - -To fine tune the proxy you can use the original [object](https://www.w3.org/TR/webdriver/#proxy) and pass it to capabilities: - -``` clojure -{:capabilities {:proxy {:proxyType "manual" - :proxyAutoconfigUrl "some.proxy.com:8080" - :ftpProxy "some.proxy.com:8080" - :httpProxy "some.proxy.com:8080" - :noProxy ["http://this.url" "http://that.url"] - :sslProxy "some.proxy.com:8080" - :socksProxy "some.proxy.com:1080" - :socksVersion 5}}} - -(chrome {:capabilities {:proxy {...}}}) -``` - -## Devtools: tracking HTTP requests, XHR (Ajax) - -With recent updates, the library brings a great feature. Now you can trace -events which come from the DevTools panel. It means, everything you see in the -developer console now is available through API. That works only with Google -Chrome now. - -To start a driver with special development settings specified, just pass an -empty map to the `:dev` field when running a driver: - -```clojure -(def c (chrome {:dev {}})) -``` - -The value must not be `nil`. When it's an empty map, a special function takes -defaults. Here is a full version of dev settings with all the possible values -specified. - -```clojure -(def c (chrome {:dev - {:perf - {:level :all - :network? true - :page? true - :interval 1000 - :categories [:devtools - :devtools.network - :devtools.timeline]}}})) -``` - -Under the hood, it fills a special `perfLoggingPrefs` dictionary inside the -`chromeOptions` object. - -Now that your browser accumulates these events, you can read them using a -special `dev` namespace. - -```clojure -(go c "http://google.com") -;; wait until the page gets loaded - -;; load the namespace -(require '[etaoin.dev :as dev]) -``` - -Let's have a list of ALL the HTTP requests happened during the page was loading. - -```clojure -(def reqs (dev/get-requests c)) - -;; reqs is a vector of maps -(count reqs) -;; 19 - -;; what were their types? -(set (map :type reqs)) -;; #{:script :other :document :image :xhr} -;; we've got Js requests, images, AJAX and other stuff -``` - -```clojure -;; check the last one request, it's an image named tia.png -(-> reqs last clojure.pprint/pprint) - -{:state 4, - :id "1000052292.8", - :type :image, - :xhr? false, - :url "https://www.gstatic.com/inputtools/images/tia.png", - :with-data? nil, - :request - {:method :get, - :headers - {:Referer "https://www.google.com/", - :User-Agent - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"}}, - :response - {:status 200, - :headers {}, ;; truncated - :mime "image/png", - :remote-ip "173.194.73.94"}, - :done? true} -``` - -Since we're mostly interested in AJAX requests, there is a function `get-ajax` -that does the same but filters XHR requests: - -```clojure -(-> c dev/get-ajax last clojure.pprint/pprint) - -{:state 4, - :id "1000051989.41", - :type :xhr, - :xhr? true, - :url - "https://www.google.com/complete/search?q=clojure%20spec&cp=12&client=psy-ab&xssi=t&gs_ri=gws-wiz&hl=ru&authuser=0&psi=4iUbXdapJsbmrgTVt7H4BA.1562060259137&ei=4iUbXdapJsbmrgTVt7H4BA", - :with-data? nil, - :request - {:method :get, - :headers - {:Referer "https://www.google.com/", - :User-Agent - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"}}, - :response - {:status 200, - :headers {}, ;; truncated - :mime "application/json", - :remote-ip "74.125.131.99"}, - :done? true} -``` - -A typical pattern of `get-ajax` usage is the following. You'd like to check if a -certain request has been fired to the server. So you press a button, wait for a -while and then read the requests made by your browser. - -Having a list of requests, you search for the one you need (e.g. by its URL) and -then check its state. The `:state` field's got the same semantics like the -`XMLHttpRequest.readyState` has. It's an integer from 1 to 4 with the same -behavior. - -To check if a request has been finished, done or failed, use these predicates: - -```clojure -(def req (last reqs)) - -(dev/request-done? req) -;; true - -(dev/request-failed? req) -;; false - -(dev/request-success? req) -;; true -``` - -Note that `request-done?` doesn't mean the request has succeeded. It only means -its pipeline has reached a final step. - -**Warning:** when you read dev logs, you consume them from an internal buffer -which gets flushed. The second call to `get-requests` or `get-ajax` will return -an empty list. - -Perhaps you want to collect these logs by your own. A function -`dev/get-performance-logs` return a list of logs so you accumulate them in an -atom or whatever: - -```clojure -(def logs (atom [])) - -;; repeat that form from time to time -(do (swap! logs concat (dev/get-performance-logs c)) - true) - -(count @logs) -;; 76 -``` - -There are `logs->requests` and `logs->ajax` functions that convert logs into -requests. Unlike `get-requests` and `get-ajax`, they are pure functions and won't -flush anything. - -```clojure -(dev/logs->requests @logs) -``` - -When working with logs and requests, pay attention it their count and size. The -maps have got plenty of keys and the amount of items in collections might be -huge. Printing a whole bunch of events might freeze your editor. Consider using -`clojure.pprint/pprint` function as it relies on max level and length limits. - -## Postmortem: auto-save artifacts in case of exception - -Sometimes, it might be difficult to discover what went wrong during the last UI -tests session. A special macro `with-postmortem` saves some useful data on disk -before the exception was triggered. Those data are a screenshot, HTML code and -JS console logs. Note: not all browsers support getting JS logs. - -Example: - -```clojure -(def driver (chrome)) -(with-postmortem driver {:dir "/Users/ivan/artifacts"} - (click driver :non-existing-element)) -``` - -An exception will rise, but in `/Users/ivan/artifacts` there will be three files -named by a template `---.`: - -- `firefox-127.0.0.1-4444-2017-03-26-02-45-07.png`: an actual screenshot of the - browser's page; -- `firefox-127.0.0.1-4444-2017-03-26-02-45-07.html`: the current browser's HTML - content; -- `firefox-127.0.0.1-4444-2017-03-26-02-45-07.json`: a JSON file with console - logs; those are a vector of objects. - -The handler takes a map of options with the following keys. All of them might be -absent. - -```clojure -{;; default directory where to store artifacts - ;; might not exist, will be created otherwise. pwd is used when not passed - :dir "/home/ivan/UI-tests" - - ;; a directory where to store screenshots; :dir is used when not passed - :dir-img "/home/ivan/UI-tests/screenshots" - - ;; the same but for HTML sources - :dir-src "/home/ivan/UI-tests/HTML" - - ;; the same but for console logs - :dir-log "/home/ivan/UI-tests/console" - - ;; a string template to format a date; See SimpleDateFormat Java class - :date-format "yyyy-MM-dd-HH-mm-ss"} -``` - -## Reading browser's logs - -Function `(get-logs driver)` returns the browser's logs as a vector of -maps. Each map has the following structure: - -```clojure -{:level :warning, - :message "1,2,3,4 anonymous (:1)", - :timestamp 1511449388366, - :source nil, - :datetime #inst "2017-11-23T15:03:08.366-00:00"} -``` - -Currently, logs are available in Chrome and Phantom.js only. Please note, the -message text and the source type highly depend on the browser. Chrome wipes the -logs once they have been read. Phantom.js keeps them but only until you change -the page. - -## Additional parameters - -When running a driver instance, a map of additional parameters might be passed -to tweak the browser's behaviour: - -```clojure -(def driver (chrome {:path "/path/to/driver/binary"})) -``` - -Below, here is a map of parameters the library support. All of them might be -skipped or have nil values. Some of them, if not passed, are taken from the -`defaults` map. - -```clojure -{;; Host and port for webdriver's process. Both are taken from defaults - ;; when are not passed. If you pass a port that has been already taken, - ;; the library will try to take a random one instead. - :host "127.0.0.1" - :port 9999 - - ;; Path to webdriver's binary file. Taken from defaults when not passed. - :path-driver "/Users/ivan/Downloads/geckodriver" - - ;; Path to the driver's binary file. When not passed, the driver discovers it - ;; by its own. - :path-browser "/Users/ivan/Downloads/firefox/firefox" - - ;; Extra command line arguments sent to the browser's process. See your browser's - ;; supported flags. - :args ["--incognito" "--app" "http://example.com"] - - ;; Extra command line arguments sent to the webdriver's process. - :args-driver ["-b" "/path/to/firefox/binary"] - - ;; Sets browser's minimal logging level. Only messages with level above - ;; that one will be collected. Useful for fetching Javascript logs. Possible - ;; values are: nil (aliases :off, :none), :debug, :info, :warn (alias :warning), - ;; :err (aliases :error, :severe, :crit, :critical), :all. When not passed, - ;; :all is set. - :log-level :err ;; to show only errors but not debug - - ;; Sets driver's log level. - ;; The value is a string. Possible values are: - ;; chrome: [ALL, DEBUG, INFO, WARNING, SEVERE, OFF] - ;; phantomjs: [ERROR, WARN, INFO, DEBUG] (default INFO) - ;; firefox [fatal, error, warn, info, config, debug, trace] - :driver-log-level - - ;; Paths to the driver's log files as strings. - ;; When not set, the output goes to /dev/null (or NUL on Windows) - :log-stdout - :log-stderr - - ;; Path to a custorm browser profile. See the section below. - :profile "/Users/ivan/Library/Application Support/Firefox/Profiles/iy4iitbg.Test" - - ;; Env variables sent to the driver's process. - :env {:MOZ_CRASHREPORTER_URL "http://test.com"} - - ;; Initial window size. - :size [1024 680] - - ;; Default URL to open. Works only in FF for now. - :url "http://example.com" - - ;; Where to download files. - :download-dir "/Users/ivan/Desktop" - - ;; Driver-specific options. Make sure you have read the docs before setting them. - :capabilities {:chromeOptions {:args ["--headless"]}}} -``` - -## Eager page load - -When you navigate to a certain page, the driver waits until the whole page has -been completely loaded. What's fine in most of the cases yet doesn't reflect the -way human beings interact with the Internet. - -Change this default behavior with the `:load-strategy` option. There are three -possible values for that: `:none`, `:eager` and `:normal` which is the default -when not passed. - -When you pass `:none`, the driver responds immediately so you are welcome to -execute next instructions. For example: - -```clojure -(def c (chrome)) -(go c "http://some.slow.site.com") -;; you'll hang on this line until the page loads -(do-something) -``` - -Now when passing the load strategy option: - -```clojure -(def c (chrome {:load-strategy :none})) -(go c "http://some.slow.site.com") -;; no pause, acts immediately -(do-something) -``` - -For the `:eager` option, it works only with Firefox at the moment of adding the -feature to the library. - - -## Keyboard chords - -There is an option to input a series of keys simultaneously. That is useful to -imitate holding a system key like Control, Shift or whatever when typing. - -The namespace `etaoin.keys` carries a bunch of key constants as well as a set of -functions related to input. - -```clojure -(require '[etaoin.keys :as keys]) -``` - -A quick example of entering ordinary characters holding Shift: - -```clojure -(def c (chrome)) -(go c "http://google.com") - -(fill-active c (keys/with-shift "caps is great")) -``` - -The main input gets populated with "CAPS IS GREAT". Now you'd like to delete the -last word. In Chrome, this is done by pressing backspace holding Alt. Let's do -that: - -```clojure -(fill-active c (keys/with-alt keys/backspace)) -``` - -Now you've got only "CAPS IS " in the input. - -Consider a more complex example which repeats real users' behaviour. You'd like -to delete everything from the input. First, you move the caret at the very -beginning. Then move it to the end holding shift so everything gets -selected. Finally, you press delete to clear the selected text. - -The combo is: - -```clojure -(fill-active c keys/home (keys/with-shift keys/end) keys/delete) -``` - -There are also `with-ctrl` and `with-command` functions that act the same. - -Pay attention, these functions do not apply to the global browser's -shortcuts. For example, neither "Command + R" nor "Command + T" reload the page -or open a new tab. - -All the `keys/with-*` functions are just wrappers upon the `keys/chord` function -that might be used for complex cases. - - -## File download directory - -To specify your own directory where to download files, pass `:download-dir` -parameter into an option map when running a driver: - -```clojure -(def driver (chrome {:download-dir "/Users/ivan/Desktop"})) -``` - -Now, once you click on a link, a file should be put into that folder. Currently, -only Chrome and Firefox are supported. - -Firefox requires to specify MIME-types of those files that should be downloaded -without showing a system dialog. By default, when the `:download-dir` parameter -is passed, the library adds the most common MIME-types: archives, media files, -office documents, etc. If you need to add your own one, override that preference -manually: - -```clojure -(def driver (firefox {:download-dir "/Users/ivan/Desktop" - :prefs {:browser.helperApps.neverAsk.saveToDisk - "some-mime/type-1;other-mime/type-2"}})) -``` - -To check whether a file was downloaded during UI tests, see the testing section -below. - -## Setting browser profile - -When running Chrome or Firefox, you may specify a special profile made for test -purposes. A profile is a folder that keeps browser settings, history, bookmarks -and other user-specific data. - -Imagine you'd like to run your integration tests against a user that turned off -Javascript execution or image rendering. To prepare a special profile for that -task would be a good choice. - -### Create and find a profile in Chrome - -1. In the right top corner of the main window, click on a user button. -2. In the dropdown, select "Manage People". -3. Click "Add person", submit a name and press "Save". -4. The new browser window should appear. Now, setup the new profile as you want. -5. Open `chrome://version/` page. Copy the file path that is beneath the - `Profile Path` caption. - -### Create and find a profile in Firefox - -[ff-profile]:https://support.mozilla.org/en-US/kb/profile-manager-create-and-remove-firefox-profiles - -1. Run Firefox with `-P`, `-p` or `-ProfileManager` key as the [official - page][ff-profile] describes. -2. Create a new profile and run the browser. -3. Setup the profile as you need. -4. Open `about:support` page. Near the `Profile Folder` caption, press the `Show - in Finder` button. A new folder window should appear. Copy its path from - there. - -### Running a driver with a profile - -Once you've got a profile path, launch a driver with a special `:profile` key as -follows: - -```clojure -;; Chrome -(def chrome-profile - "/Users/ivan/Library/Application Support/Google/Chrome/Profile 2/Default") - -(def chrm (chrome {:profile chrome-profile})) - -;; Firefox -(def ff-profile - "/Users/ivan/Library/Application Support/Firefox/Profiles/iy4iitbg.Test") - -(def ff (firefox {:profile ff-profile})) -``` - -## Scrolling - -The library ships a set of functions to scroll the page. - -The most important one, `scroll-query` jumps the the first element found with -the query term: - -```clojure -(def driver (chrome)) - -;; the form button placed somewhere below -(scroll-query driver :button-submit) - -;; the main article -(scroll-query driver {:tag :h1}) -``` - -To jump to the absolute position, just use `scroll` as follows: - -```clojure -(scroll driver 100 600) - -;; or pass a map with x and y keys -(scroll driver {:x 100 :y 600}) -``` - -To scroll relatively, use `scroll-by` with offset values: - -```clojure -;; keeps the same horizontal position, goes up for 100 pixels -(scroll-by driver 0 -100) ;; map parameter is also supported -``` - -There are two shortcuts to jump top or bottom of the page: - -```clojure -(scroll-bottom driver) ;; you'll see the footer... -(scroll-top driver) ;; ...and the header again -``` - -The following functions scroll the page in all directions: - -```clojure -(scroll-down driver 200) ;; scrolls down by 200 pixels -(scroll-down driver) ;; scrolls down by the default (100) number of pixels - -(scroll-up driver 200) ;; the same, but scrolls up... -(scroll-up driver) - -(scroll-left driver 200) ;; ...left -(scroll-left driver) - -(scroll-right driver 200) ;; ... and right -(scroll-right driver) -``` - -One note, in all cases the scroll actions are served with Javascript. Ensure -your browser has it enabled. - -## Working with frames and iframes - -While working with the page, you cannot interact with those items that are put -into a frame or an iframe. The functions below switch the current context on -specific frame: - -```clojure -(switch-frame driver :frameId) ;; now you are inside an iframe with id="frameId" -(click driver :someButton) ;; click on a button inside that iframe -(switch-frame-top driver) ;; switches on the top of the page again -``` - -Frames could be nested one into another. The functions take that into -account. Say you have an HTML layout like this: - -```html - + +---- + +Let's explore switching to `:frame1`. + +[source,clojure] +---- +(e/go driver sample-page) +;; we start in the main page, we can't see inside frame1: +(e/exists? driver :in-frame1) +;; => false + +;; switch context to frame with id of frame1: +(e/switch-frame driver :frame1) + +;; now we can interact with elements in frame1: +(e/exists? driver :in-frame1) +;; => true +(e/get-element-text driver :in-frame1) +;; => "In frame1 paragraph" + +;; switch back to top frame (the main page) +(e/switch-frame-top driver) +---- + +To reach nested frames, you can dig down like so: + +[source,clojure] +---- +;; switch to the first top-level iframe with the main page: frame1 +(e/switch-frame-first driver) +;; downward to the first iframe with frame1: frame2 +(e/switch-frame-first driver) +(e/get-element-text driver :in-frame2) +;; => "In frame2 paragraph" +;; back up to frame1 +(e/switch-frame-parent driver) +;; back up to main page +(e/switch-frame-parent driver) +---- + +Use the `with-frame` macro to temporarily switch to a target frame, do some work, returning its last expression, while preserving your original frame context. + +[source,clojure] +---- +(e/with-frame driver {:id :frame1} + (e/with-frame driver {:id :frame2} + (e/get-element-text driver :in-frame2))) +;; => "In frame2 paragraph" +---- + +=== Executing Javascript + +Use `js-execute` to evaluate a Javascript code in the browser: + +[source,clojure] +---- +(e/js-execute driver "alert('Hello from Etaoin!')") +(e/dismiss-alert driver) +---- + +Pass any additional parameters to the script with the `arguments` array-like object. +[source,clojure] +---- +(e/js-execute driver "alert(arguments[2].foo)" 1 false {:foo "hello again!"}) +(e/dismiss-alert driver) +---- + +We have passed 3 arguments: + +. `1` +. `false` +. `{:foo "hello again!}` which is automatically converted to JSON `{"foo": "hello again!"}` + +The alert then presents the `foo` field of the 3rd (index 2) argument, which is `"hello again!"`. + +To return any data to Clojure, add `return` into your script: + +[source,clojure] +---- +(e/js-execute driver "return {foo: arguments[2].foo, bar: [1, 2, 3]}" + ;; same args as previous example: + 1 false {:foo "hello again!"}) +;; => {:bar [1 2 3], :foo "hello again!"} +---- + +Notice that the JSON has been automatically converted to edn. + +==== Asynchronous Scripts + +Use `js-async` to deal with scripts that rely on async strategies such as `setTimeout`. +The WebDriver creates and passes a callback as the last argument to your script. +To indicate that work is complete, you must call this callback. + +Example: + +[source,clojure] +---- +(e/js-async + driver + "var args = arguments; // preserve the global args + // WebDriver added the callback as the last arg, we grab it here + var callback = args[args.length-1]; + setTimeout(function() { + // We call the WebDriver callback passing with what we want it to return + // In this case we pass we chose to return 42 from the arg we passed in + callback(args[0].foo.bar.baz); + }, + 1000);" + {:foo {:bar {:baz 42}}}) +;; => 42 +---- + +If you'd like to override the default script timeout, you can do so for the WebDriver session: + +[source,clojure] +---- +;; optionally save the current value for later restoration +(def orig-script-timeout (e/get-script-timeout driver)) +(e/set-script-timeout driver 5) ;; in seconds +;; do some stuff +(e/set-script-timeout driver orig-script-timeout) +---- + +or for a block of code via `with-script-timeout`: + +//:test-doc-blocks/skip +[source,clojure] +---- +(e/with-script-timeout driver 30 + (e/js-async driver "var callback = arguments[arguments.length-1]; + //some long operation here + callback('phew,done!');")) +;; => "phew,done!" +---- + +=== Wait Functions + +The main difference between a program and a human being is that the first one operates very fast. +A computer operates so fast, that sometimes a browser cannot render new HTML in time. +After each action, you might consider including a `wait-` function that polls a browser until the predicate evaluates to true. +Or just `(wait )` if you don't care about optimization. + +The `with-wait` macro might be helpful when you need to prepend each action with `(wait n)`. +For example, the following form: + +[source,clojure] +---- +(e/with-wait 1 + (e/refresh driver) + (e/fill driver :uname "my username") + (e/fill driver :text "some text")) +---- + +is executed something along the lines of: + +[source,clojure] +---- +(e/wait 1) +(e/refresh driver) +(e/wait 1) +(e/fill driver :uname "my username") +(e/wait 1) +(e/fill driver :text "some text") +---- + +and thus returns the result of the last form of the original body. + +The `(doto-wait n driver & body)` acts like the standard `doto` but prepends each form with `(wait n)`. +The above example re-expressed with `doto-wait`: + +[source,clojure] +---- +(e/doto-wait 1 driver + (e/refresh) + (e/fill :uname "my username") + (e/fill :text "some text")) +---- + +This is effectively the same as: + +[source,clojure] +---- +(doto driver + (e/wait 1) + (e/refresh) + (e/wait 1) + (e/fill :uname "my username") + (e/wait 1) + (e/fill :text "some text")) +---- + +In addition to `with-wait` and `do-wait` there are a number of waiting functions: `wait-visible`, `wait-has-alert`, `wait-predicate`, etc (see the full list in the link:{url-doc}/CURRENT/api/etaoin.api#wait[API docs]. +They accept default timeout/interval values that can be redefined using the `with-wait-timeout` and `with-wait-interval` macros, respectively. +They all throw if the wait timeout is exceeded. + +[source,clojure] +---- +(e/with-wait-timeout 15 ;; time in seconds + (doto driver + (e/refresh) + (e/wait-visible {:id :last-section}) + (e/click {:tag :a}) + (e/wait-has-text :clicked "link 1"))) +---- + +Wait text: + +* `wait-has-text` waits until an element has text anywhere inside it (including inner HTML). ++ +[source,clojure] +---- +(e/click driver {:tag :a}) +(e/wait-has-text driver :clicked "link 1") +---- + +* `wait-has-text-everywhere` like `wait-has-text` but searches for text across the entire page ++ +[source,clojure] +---- +(e/wait-has-text-everywhere driver "ipsum") +---- + +=== Load Strategy [[load-strategy]] + +When you navigate to a page, the driver waits until the whole page has been completely loaded. +That's fine in most cases but doesn't reflect the way human beings interact with the Internet. + +Change this default behavior with the `:load-strategy` option: + +* `:normal` (the default) wait for full page load (everything, include images, etc) +* `:none` don't wait at all +* `:eager` wait for only DOM content to load + +For example, the default `:normal` strategy: + +[source,clojure] +---- +(e2/with-chrome [driver] + (e/go driver sample-page) + ;; by default you'll hang on this line until the page loads + ;; (do-something) +) +---- + +Load strategy option of `:none`: + +[source,clojure] +---- +(e2/with-chrome [driver {:load-strategy :none}] + (e/go driver sample-page) + ;; no pause, no waiting, acts immediately + ;; (do-something) +) +---- + +The `:eager` option only works with Firefox at the moment. + +=== Actions [[actions]] + +Etaoin supports link:{actions}[Webdriver Actions]. +They are described as "virtual input devices". +They act as little device input scripts that run simultaneously. + +Here, in raw form, we have an example of two actions. +One controls the keyboard, the other the pointer (mouse). + +[source,clojure] +---- +;; a keyboard input +{:type "key" + :id "some name" + :actions [{:type "keyDown" :value "a"} + {:type "keyUp" :value "a"} + {:type "pause" :duration 100}]} +;; some pointer input +{:type "pointer" + :id "UUID or some name" + :parameters {:pointerType "mouse"} + :actions [{:type "pointerMove" :origin "pointer" :x 396 :y 323} + ;; double click + {:type "pointerDown" :duration 0 :button 0} + {:type "pointerUp" :duration 0 :button 0} + {:type "pointerDown" :duration 0 :button 0} + {:type "pointerUp" :duration 0 :button 0}]} +---- + +You can create a map manually and send it to the `perform-actions` method: + +[source,clojure] +---- +(def keyboard-input {:type "key" + :id "some name" + :actions [{:type "keyDown" :value "e"} + {:type "keyUp" :value "e"} + {:type "keyDown" :value "t"} + {:type "keyUp" :value "t"} + ;; duration is in ms + {:type "pause" :duration 100}]}) +;; refresh so that we'll be at the active input field +(e/refresh driver) +;; perform our keyboard input action +(e/perform-actions driver keyboard-input) +---- + +Or you might choose to use Etaoin's action helpers. +First you create the virtual input device: + +[source,clojure] +---- +(def keyboard (e/make-key-input)) +---- + +and then fill it with the actions: + +[source,clojure] +---- +(-> keyboard + (e/add-key-down k/shift-left) + (e/add-key-down "a") + (e/add-key-up "a") + (e/add-key-up k/shift-left)) +---- + +Here's a slightly larger working annotated example: + +[source,clojure] +---- +;; virtual inputs run simultaneously so we'll create a little helper to generate n pauses +(defn add-pauses [input n] + (->> (iterate e/add-pause input) + (take (inc n)) + last)) + +(let [username (e/query driver :uname) + submit-button (e/query driver {:tag :button}) + mouse (-> (e/make-mouse-input) + ;; click on username + (e/add-pointer-click-el + username k/mouse-left) + ;; pause 10 clicks to allow keyboard action to enter username + ;; (key up and down for each of keypress for etaoin) + (add-pauses 10) + ;; click on submit button + (e/add-pointer-click-el + submit-button k/mouse-left)) + keyboard (-> (e/make-key-input) + ;; pause 2 ticks to allow mouse action to first click on username + ;; (move to username element + click on it) + (add-pauses 2) + (e/with-key-down k/shift-left + (e/add-key-press "e")) + (e/add-key-press "t") + (e/add-key-press "a") + (e/add-key-press "o") + (e/add-key-press "i") + (e/add-key-press "n")) ] + (e/perform-actions driver keyboard mouse)) +---- + +To clear the state of virtual input devices, release all currently pressed keys etc, use the `release-actions` method: + +[source,clojure] +---- +(e/release-actions driver) +---- + +== Capturing Screenshots + +Calling the `screenshot` function dumps the current visible page into a PNG image file on your disk. +Specify any absolute or relative path. +Specify a string: + +[source,clojure] +---- +(e/screenshot driver "target/etaoin-play/page.png") +---- + +or a `File` object: + +[source,clojure] +---- +(require '[clojure.java.io :as io]) +(e/screenshot driver (io/file "target/etaoin-play/test.png")) +---- + +=== Screenshots for Specific Elements + +With Firefox and Chrome, you can also capture a single element within a page, say a div, an input widget, or whatever. +It doesn't work with other browsers at this time. + +[source,clojure] +---- +(e/screenshot-element driver {:tag :form :class :formy} "target/etaoin-play/form-element.png") +---- + +=== Screenshots after each form + +Use `with-screenshots` to take a screenshot to the specified directory after each form is executed in the code block. +The file naming convention is `-.png` + +[source,clojure] +---- +(require '[clojure.java.io :as io]) + +(e/refresh driver) +(.mkdirs (io/file "target/etaoin-play/saved-screenshots")) +(e/with-screenshots driver "target/etaoin-play/saved-screenshots" + (e/fill driver :uname "et") + (e/fill driver :uname "ao") + (e/fill driver :uname "in")) +---- + +this is equivalent to something along the lines of: + +[source,clojure] +---- +(require '[clojure.java.io :as io]) + +(e/refresh driver) +(.mkdirs (io/file "target/etaoin-play/saved-screenshots")) +(e/fill driver :uname "et") +(e/screenshot driver "target/etaoin-play/saved-screenshots/chrome-1.png") +(e/fill driver :uname "ao") +(e/screenshot driver "target/etaoin-play/saved-screenshots/chrome-2.png") +(e/fill driver :uname "in") +(e/screenshot driver "target/etaoin-play/saved-screenshots/chrome-3.png") +---- + +== Peeking deeper + +Sometimes it is useful to peek a little deeper. + +=== Reading a Browser's Console Logs [[console-logs]] + +Function `get-logs` returns the browser's console logs as a vector of maps. +Each map has the following structure: + +// note that we do not verify get-logs output with test-doc-blocks by omitting => +[source,clojure] +---- +(e/js-execute driver "console.log('foo')") +(e/get-logs driver) +;; [{:level :info, +;; :message "console-api 2:32 \"foo\"", +;; :source :console-api, +;; :timestamp 1654358994253, +;; :datetime #inst "2022-06-04T16:09:54.253-00:00"}] + +;; on the 2nd call, for chrome, we'll find the logs empty +(e/get-logs driver) +;; => [] +---- + +Currently, logs are available in Chrome and Phantom.js only. +The message text and the source type will vary by browser vendor. +Chrome wipes the logs once they have been read. +Phantom.js wipes the logs when the page location changes. + +=== DevTools: Tracking HTTP Requests, XHR (Ajax) [[devtools]] + +You can trace events that come from the DevTools panel. +This means that everything you see in the developer console now is available through the Etaoin API. +This currently only works for Google Chrome. + +To start a driver with devtools support enabled specify a `:dev` map. + +//let's put this drier in its own namespace +//{:test-doc-blocks/test-ns user-guide-devtools-test} +[source,clojure] +---- +(require '[etaoin.api2 :as e2]) + +(e2/with-chrome [driver {:dev {}}] + ;; do some stuff +) +---- + +The value must not be a map (not `nil`). +When `:dev` an empty map, the following defaults are used. + +[source,clojure] +---- +{:perf + {:level :all + :network? true + :page? false + :categories [:devtools.network] + :interval 1000}} +---- + +We'll work with a driver that enables everything: + +//{:test-doc-blocks/test-ns user-guide-devtools-test} +[source,clojure] +---- +(require '[etaoin.api :as e]) + +(def driver (e/chrome {:dev + {:perf + {:level :all + :network? true + :page? true + :interval 1000 + :categories [:devtools + :devtools.network + :devtools.timeline]}}})) +---- + +Under the hood, Etaoin sets up a special `perfLoggingPrefs` dictionary inside the `chromeOptions` object. + +Now that your browser is accumulating these events, you can read them using a special `dev` namespace. + +The results will be different when you try this, but here's what I experienced: + +//{:test-doc-blocks/test-ns user-guide-devtools-test} +[source,clojure] +---- +(require '[etaoin.dev :as dev]) + +(e/go driver "https://google.com") + +(def reqs (dev/get-requests driver)) + +;; reqs is a vector of maps +(count reqs) +;; 23 + +;; what were the request types? +(frequencies (map :type reqs)) +;; {:script 6, +;; :other 2, +;; :xhr 4, +;; :image 5, +;; :stylesheet 1, +;; :ping 3, +;; :document 1, +;; :manifest 1} + +;; Interesting, we've got Js requests, images, AJAX and other stuff +---- + +//{:test-doc-blocks/test-ns user-guide-devtools-test} +[source,clojure] +---- +;; let's take a peek at the last image: +(last (filter #(= :image (:type %)) reqs)) +;; {:state 4, +;; :id "14535.6", +;; :type :image, +;; :xhr? false, +;; :url +;; "https://www.google.com/images/searchbox/desktop_searchbox_sprites318_hr.webp", +;; :with-data? nil, +;; :request +;; {:method :get, +;; :headers +;; {:Referer "https://www.google.com/?gws_rd=ssl", +;; :sec-ch-ua-full-version-list +;; "\" Not A;Brand\";v=\"99.0.0.0\", \"Chromium\";v=\"102.0.5005.61\", \"Google Chrome\";v=\"102.0.5005.61\"", +;; :sec-ch-viewport-width "1200", +;; :sec-ch-ua-platform-version "\"10.15.7\"", +;; :sec-ch-ua +;; "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"102\", \"Google Chrome\";v=\"102\"", +;; :sec-ch-ua-platform "\"macOS\"", +;; :sec-ch-ua-full-version "\"102.0.5005.61\"", +;; :sec-ch-ua-wow64 "?0", +;; :sec-ch-ua-model "", +;; :sec-ch-ua-bitness "\"64\"", +;; :sec-ch-ua-mobile "?0", +;; :sec-ch-dpr "1", +;; :sec-ch-ua-arch "\"x86\"", +;; :User-Agent +;; "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.61 Safari/537.36"}}, +;; :response +;; {:status nil, +;; :headers +;; {:date "Sat, 04 Jun 2022 00:11:36 GMT", +;; :x-xss-protection "0", +;; :x-content-type-options "nosniff", +;; :server "sffe", +;; :cross-origin-opener-policy-report-only +;; "same-origin; report-to=\"static-on-bigtable\"", +;; :last-modified "Wed, 22 Apr 2020 22:00:00 GMT", +;; :expires "Sat, 04 Jun 2022 00:11:36 GMT", +;; :cache-control "private, max-age=31536000", +;; :content-length "660", +;; :report-to +;; "{\"group\":\"static-on-bigtable\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/static-on-bigtable\"}]}", +;; :alt-svc +;; "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"", +;; :cross-origin-resource-policy "cross-origin", +;; :content-type "image/webp", +;; :accept-ranges "bytes"}, +;; :mime "image/webp", +;; :remote-ip "142.251.41.68"}, +;; :done? true} +---- + +TIP: The details of these responses come from Chrome and are subject to changes to Chrome. + +Since we're mostly interested in AJAX requests, there is a function `get-ajax` that does the same but filters XHR requests: + +//{:test-doc-blocks/test-ns user-guide-devtools-test} +[source,clojure] +---- +;; refresh to fill the logs again +(e/go driver "https://google.com") +(e/wait 2) ;; give ajax requests a chance to finish + +(last (dev/get-ajax driver)) +;; {:state 4, +;; :id "14535.59", +;; :type :xhr, +;; :xhr? true, +;; :url +;; "https://www.google.com/complete/search?q&cp=0&client=gws-wiz&xssi=t&hl=en-CA&authuser=0&psi=OtuaYq-xHNeMtQbkjo6gBg.1654315834852&nolsbt=1&dpr=1", +;; :with-data? nil, +;; :request +;; {:method :get, +;; :headers +;; {:Referer "https://www.google.com/", +;; :sec-ch-ua-full-version-list +;; "\" Not A;Brand\";v=\"99.0.0.0\", \"Chromium\";v=\"102.0.5005.61\", \"Google Chrome\";v=\"102.0.5005.61\"", +;; :sec-ch-viewport-width "1200", +;; :sec-ch-ua-platform-version "\"10.15.7\"", +;; :sec-ch-ua +;; "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"102\", \"Google Chrome\";v=\"102\"", +;; :sec-ch-ua-platform "\"macOS\"", +;; :sec-ch-ua-full-version "\"102.0.5005.61\"", +;; :sec-ch-ua-wow64 "?0", +;; :sec-ch-ua-model "", +;; :sec-ch-ua-bitness "\"64\"", +;; :sec-ch-ua-mobile "?0", +;; :sec-ch-dpr "1", +;; :sec-ch-ua-arch "\"x86\"", +;; :User-Agent +;; "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.61 Safari/537.36"}}, +;; :response +;; {:status nil, +;; :headers +;; {:bfcache-opt-in "unload", +;; :date "Sat, 04 Jun 2022 04:10:35 GMT", +;; :content-disposition "attachment; filename=\"f.txt\"", +;; :x-xss-protection "0", +;; :server "gws", +;; :expires "Sat, 04 Jun 2022 04:10:35 GMT", +;; :accept-ch +;; "Sec-CH-Viewport-Width, Sec-CH-Viewport-Height, Sec-CH-DPR, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, Sec-CH-UA-Full-Version, Sec-CH-UA-Arch, Sec-CH-UA-Model, Sec-CH-UA-Bitness, Sec-CH-UA-Full-Version-List, Sec-CH-UA-WoW64", +;; :cache-control "private, max-age=3600", +;; :report-to +;; "{\"group\":\"gws\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/gws/cdt1\"}]}", +;; :x-frame-options "SAMEORIGIN", +;; :strict-transport-security "max-age=31536000", +;; :content-security-policy +;; "object-src 'none';base-uri 'self';script-src 'nonce-xM7BqmSpeu5Zd6usKOP4JA' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/cdt1", +;; :alt-svc +;; "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"", +;; :content-type "application/json; charset=UTF-8", +;; :cross-origin-opener-policy "same-origin-allow-popups; report-to=\"gws\"", +;; :content-encoding "br"}, +;; :mime "application/json", +;; :remote-ip "142.251.41.36"}, +;; :done? true};; => nil +---- + +A typical pattern of `get-ajax` usage is the following. +You'd like to check if a certain request has been fired to the server. +So you press a button, wait for a while, and then read the requests made by your browser. + +Having a list of requests, you search for the one you need (e.g. by its URL) and then check its state. +The `:state` field has got the same semantics of the `XMLHttpRequest.readyState`. +It's an integer from 1 to 4 with the same behavior. + +To check if a request has been finished, done or failed, use these predicates: + +//{:test-doc-blocks/test-ns user-guide-devtools-test} +[source,clojure] +---- +;; fill the logs +(e/go driver "https://google.com") +(e/wait 2) ;; give ajax requests a chance to finish + +(def reqs (dev/get-ajax driver)) +;; you'd search for what you are interested in here +(def req (last reqs)) + +(dev/request-done? req) +;; => true + +(dev/request-failed? req) +;; => nil + +(dev/request-success? req) +;; => true +---- + +Note that `request-done?` doesn't mean the request has succeeded. +It only means its pipeline has reached a final step. + +TIP: when you read dev logs, you consume them from an internal buffer that gets flushed. +The second call to `get-requests` or `get-ajax` will return an empty list. + +Perhaps you want to collect these logs. +A function `dev/get-performance-logs` return a list of logs so you accumulate them in an atom or whatever: + +//{:test-doc-blocks/test-ns user-guide-devtools-test} +[source,clojure] +---- +;; setup a collector +(def logs (atom [])) + +;; make requests +(e/refresh driver) + +;; collect as needed +(do (swap! logs concat (dev/get-performance-logs driver)) + true) + +(count @logs) +;; 136 +---- + +The `+logs->requests+` and `+logs->ajax+` functions convert already fetched logs into requests. +Unlike `get-requests` and `get-ajax`, they are pure functions and won't flush anything. + +//{:test-doc-blocks/test-ns user-guide-devtools-test} +[source,clojure] +---- +;; convert our fetched requests from our collector atom +(dev/logs->requests @logs) +(last (dev/logs->requests @logs)) +;; {:state 4, +;; :id "14535.162", +;; :type :ping, +;; :xhr? false, +;; :url +;; "https://www.google.com/gen_204?atyp=i&r=1&ei=Zd2aYsrzLozStQbzgbqIBQ&ct=slh&v=t1&m=HV&pv=0.48715273690818806&me=1:1654316389931,V,0,0,1200,1053:0,B,1053:0,N,1,Zd2aYsrzLozStQbzgbqIBQ:0,R,1,1,0,0,1200,1053:93,x:42832,e,U&zx=1654316432856", +;; :with-data? true, +;; :request +;; {:method :post, +;; :headers +;; {:Referer "https://www.google.com/", +;; :sec-ch-ua-full-version-list +;; "\" Not A;Brand\";v=\"99.0.0.0\", \"Chromium\";v=\"102.0.5005.61\", \"Google Chrome\";v=\"102.0.5005.61\"", +;; :sec-ch-viewport-width "1200", +;; :sec-ch-ua-platform-version "\"10.15.7\"", +;; :sec-ch-ua +;; "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"102\", \"Google Chrome\";v=\"102\"", +;; :sec-ch-ua-platform "\"macOS\"", +;; :sec-ch-ua-full-version "\"102.0.5005.61\"", +;; :sec-ch-ua-wow64 "?0", +;; :sec-ch-ua-model "", +;; :sec-ch-ua-bitness "\"64\"", +;; :sec-ch-ua-mobile "?0", +;; :sec-ch-dpr "1", +;; :sec-ch-ua-arch "\"x86\"", +;; :User-Agent +;; "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.61 Safari/537.36"}}, +;; :response +;; {:status nil, +;; :headers +;; {:alt-svc +;; "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"", +;; :bfcache-opt-in "unload", +;; :content-length "0", +;; :content-type "text/html; charset=UTF-8", +;; :date "Sat, 04 Jun 2022 04:20:32 GMT", +;; :server "gws", +;; :x-frame-options "SAMEORIGIN", +;; :x-xss-protection "0"}, +;; :mime "text/html", +;; :remote-ip "142.251.41.36"}, +;; :done? true} +---- + +When working with logs and requests, pay attention to their count and size. +The maps have plenty of keys and the number of items in collections can become very large. +Printing a slew of events might freeze your editor. +Consider using `clojure.pprint/pprint` as it relies on max level and length limits. + +// hidden cleanup of our devtools driver +ifdef::env-test-doc-blocks[] +//{:test-doc-blocks/test-ns user-guide-devtools-test} +[source,clojure] +---- +(e/quit driver) +---- +endif::[] + +=== Postmortem: Auto-save Artifacts in Case of Exception [[postmortem]] + +Sometimes, it can be difficult to diagnose what went wrong during a failed UI test run. +Use the `with-postmortem` to save useful data to disk before the exception was triggered: + +* a screenshot of the visible browser page +* HTML code of the current browser page +* JS console logs, <> + +Example: + +[source,clojure] +---- +(try + (e/with-postmortem driver {:dir "target/etaoin-play/postmortem"} + (e/click driver :non-existing-element)) + (catch Exception _e + "yup, we threw!")) +;; => "yup, we threw!" +---- + +An exception will occur. Under `target/etaoin-postmortem` you will find three postmortem files named like so: `---.`, for example: + +[source,shell] +---- +$ tree target +target +└── etaoin-postmortem + ├── chrome-127.0.0.1-49766-2022-06-04-12-26-31.html + ├── chrome-127.0.0.1-49766-2022-06-04-12-26-31.json + └── chrome-127.0.0.1-49766-2022-06-04-12-26-31.png +---- + +The available `with-postmortem` options are: + +[source,clojure] +---- +{;; directory to save artifacts + ;; will be created if it does not already exist, defaults to current working directory + :dir "/home/ivan/UI-tests" + + ;; directory to save screenshots; defaults to :dir + :dir-img "/home/ivan/UI-tests/screenshots" + + ;; the same but for HTML sources + :dir-src "/home/ivan/UI-tests/HTML" + + ;; the same but for console logs + :dir-log "/home/ivan/UI-tests/console" + + ;; a string template to format a timestamp; See SimpleDateFormat Java class + :date-format "yyyy-MM-dd-HH-mm-ss"} +---- + +== Driver Options [[driver-options]] + +When creating a driver instance, a map of additional parameters can optionally be passed to tweak the WebDriver and web browser behaviour. + +Here, for example, we set an explicit path to the chrome WebDriver binary: + +//:test-doc-blocks/skip +[source,clojure] +---- +(def driver (e/chrome {:path-driver "/Users/ivan/downloads/chromedriver"})) +---- + +[cols="70,30"] +|=== +| Option | Defaults + +a|`:host` for *WebDriver* process. When: + +* omitted, creates a new local WebDriver process (unless `:webdriver-url` was specified). +* specified, attempts to connect to an existing running WebDriver process. +See <>. + +Alternative: see `:webdriver-url` below. + +Example: `:host "192.68.1.12"` +| + +|`:port` for *WebDriver* process. +If `:port` is found to already in use when creating a new local WebDriver process (see `:host`), a random port will be automatically selected. + +See also <>. + +Example: `:port 9997` +a| Varies by vendor: + +* chrome `9515` +* firefox `4444` +* safari `4445` +* edge `17556` +* phantom `8910` + +a| `:webdriver-url` for *WebDriver* process. When: + +* omitted, creates a new local WebDriver process (unless `:host` was specified). +* specified, attempts to connect to an existing running WebDriver process. + +See <>. + +Alternative: see `:host` above. + +Example: `"https://chrome.browserless.io/webdriver"` + +| + +| `:path-driver` to *WebDriver* binary. + +Typically used if your WebDriver is not on the PATH. + +Example: +`:path-driver "/Users/ivan/Downloads/geckodriver"` +a| As you would expect, varies by vendor: + +* chrome `"chromedriver"` +* firefox `"geckodriver"` +* safari `"safaridriver"` +* edge `"msedgedriver"` +* phantom `"phantomjs"` + +| `:args-driver` specifies extra command line arguments to *WebDriver*. + +Example: `:args-driver ["-b" "/path/to/firefox/binary"]` +| + +| `:path-browser` to *web browser* binary. + +Typically used if your browser is not on the PATH. + +Example: `:path-browser "/Users/ivan/Downloads/firefox/firefox"` +| By default, the WebDriver process automatically finds the web browser. + +| `:args` specifies extra command line arguments to *web browser*, see your web browser docs for what is available. + +Example: `:args ["--incognito" "--app" "http://example.com"]` +| + +a| `:log-level` *web browser* minimal console log level. +Only messages with this level and above will be collected. +From least to most verbose: + +* `nil`, `:off` or `:none` for no messages +* `:err`, `:error`, `:severe`, `:crit` or `:critical` +* `:warn` or `:warning` +* `:debug` +* `:all` for all messages. + +See <> + +Example: `:log-level :err` + +| `:all` + +a| `driver-log-level` *WebDriver* minimal log level. +values vary by browser driver vendor: + +* chrome `"OFF"` `"SEVERE"` `"WARNING"` `"INFO"` or `"DEBUG"` +* firefox `"fatal"` `"error"` `"warn"` `"info"` `"config"` `"debug"` or `"trace"` +* phantomjs `"ERROR"` `"WARN"` `"INFO"` `"DEBUG"` + +Example: `:driver-log-level "INFO"` + +a| * phantomjs `"INFO"` + +a| `:log-stdout` and `:log-stderr` *WebDriver* stdout and stderr log files + +Example: +[source,clojure] +---- + :log-stdout "target/chromedriver-out.log" + :log-stderr "target/chrmoedriver-err.log" +---- +| `/dev/null`, on Windows `NUL` + +| `:profile` path to custom *web browser* profile, see <> + +Example: + +`:profile "/Users/ivan/Library/Application Support/Firefox/Profiles/iy4iitbg.Test"` + +| + +| `:env` map of environment variables for *WebDriver* process. + +Example: `:env {:MOZ_CRASHREPORTER_URL "http://test.com"}` +| + +| `:size` initial *web browser* window width and height in pixels + +Example: `size: [640 480]` +| [1024 680] + +| `:url` default URL to open in *web browser*.+ +Only works in Firefox at this time. + +Example: `:url "https://clojure.org"` +| + +| `:user-agent` overrides the *web browser* `User-Agent`. +Useful for headless mode. +See <>. + +Example: `:user-agent "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"` +| Default is governed by WebDriver vendor. + +| `:download-dir` directory for *web browser* downloads files. +See <> + +Example: `:download-dir "target/chrome-downloads"` +| Default is governed by browser vendor. + +| `:headless` run the *web browser* without a UI. +See <>. + +Example `:headless true` +| Normally `false`, but automatically set for driver creation functions like `chrome-headless`, `with-chrome-headless` etc. + +| `:prefs` map of *web browser* specific preferences. + +Example: see one usage in <>. +| + +| `:proxy` to set *web browser* proxy. + +Example: see <>. +| + +| `:load-strategy` controls how long the *WebDriver* should wait before interacting with a page. +See <>. + +Example: `:load-strategy :none` +| `:normal` + +| `:capabilities` *WebDriver*-specific options. +Read vendor docs for WebDriver before setting anything here. +You'll find an example usage under <>. +| + +|=== + +=== Using Headless Drivers [[headless]] + +Google Chrome, Firefox, and Microsoft Edge can be run in headless mode. +When headless, none of the UI windows appear on the screen. +Running without a UI is helpful when: + +* running integration tests on servers that do not have a graphical output device +* running local tests without having them take over your local UI + +Ensure your browser supports headless mode by checking if it accepts `--headless` command-line argument when running it from the terminal. +The Phantom.js driver is headless by its nature (it was never been developed for rendering UI). + +When starting a driver, pass the `:headless` boolean flag to switch into headless mode. +This flag is ignored for Safari which, as of June 2022, still does not support headless mode. + +//{:test-doc-blocks/test-ns user-guide-headless-test} +[source,clojure] +---- +(require '[etaoin.api :as e]) + +(def driver (e/chrome {:headless true})) ;; runs headless Chrome +;; do some stuff +(e/quit driver) +---- + +or + +//{:test-doc-blocks/test-ns user-guide-headless-test} +[source,clojure] +---- +(def driver (e/firefox {:headless true})) ;; runs headless Firefox +;; you can also check if a driver is in headless mode: +(e/headless? driver) +;; => true +(e/quit driver) +---- + +NOTE: PhantomJS will always be in headless mode. + +There are several shortcuts to run Chrome or Firefox in headless mode: + +//{:test-doc-blocks/test-ns user-guide-headless-test} +[source,clojure] +---- +(def driver (e/chrome-headless)) +;; do some stuff +(e/quit driver) + +;; or + +(def driver (e/firefox-headless {:log-level :all})) ;; with extra settings +;; do some stuff +(e/quit driver) + +;; or + +(require '[etaoin.api2 :as e2]) + +(e2/with-chrome-headless [driver] + (e/go driver "https://clojure.org")) + +(e2/with-firefox-headless [driver {:log-level :all}] ;; extra settings + (e/go driver "https://clojure.org")) +---- + +There are also the `when-headless` and `when-not-headless` macros that conditonally execute a block of commands: + +//{:test-doc-blocks/test-ns user-guide-headless-test} +[source,clojure] +---- +(e2/with-chrome [driver] + (e/when-not-headless driver + ;;... some actions that might be not available in headless mode + ) + ;;... common actions for both versions + ) +---- + +=== File Download Directory [[download-dir]] + +To specify a directory where the browser should download files, use the `:download-dir` option: + +//:test-doc-blocks/skip +[source,clojure] +---- +(def driver (e/chrome {:download-dir "target/etaoin-play/chrome-downloads"})) +;; do some downloading +(e/driver quit) +---- + +Now, when you click on a download link, the file will be saved to that folder. +Currently, only Chrome and Firefox are supported. + +Firefox requires specifying MIME-types of the files that should be downloaded without showing a system dialog. +By default, when the `:download-dir` parameter is passed, the library adds the most common MIME-types: archives, media files, office documents, etc. +If you need to add your own one, override that Firefox preference manually via the `:prefs` option: + +//:test-doc-blocks/skip +[source,clojure] +---- +(def driver (e/firefox {:download-dir "target/etaoin-play/firefox-downloads" + :prefs {:browser.helperApps.neverAsk.saveToDisk + "some-mime/type-1;other-mime/type-2"}})) +;; do some downloading +(e/driver quit) +---- + +To check whether a file was downloaded during UI tests, see <>. + +=== Managing User-Agent [[user-agent]] + +Set a custom `User-Agent` header with the `:user-agent` option when creating a driver, for example: + +[source,clojure] +---- +(e2/with-firefox [driver {:user-agent "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"}] + (e/get-user-agent driver)) +;; => "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)" +---- + +Setting this header is important when using <> as many websites implement some sort of blocking when the User-Agent includes the "headless" string. +This can lead to 403 response or some weird behavior of the site. + +=== HTTP Proxy [[http-proxy]] + +To set proxy settings use environment variables `HTTP_PROXY`/`HTTPS_PROXY` or pass a map of the following type: + +//:test-doc-blocks/skip +[source,clojure] +---- +{:proxy {:http "some.proxy.com:8080" + :ftp "some.proxy.com:8080" + :ssl "some.proxy.com:8080" + :socks {:host "myproxy:1080" :version 5} + :bypass ["http://this.url" "http://that.url"] + :pac-url "localhost:8888"}} + +;; example +(e/chrome {:proxy {:http "some.proxy.com:8080" + :ssl "some.proxy.com:8080"}}) +---- + +NOTE: A `:pac-url` is for a https://en.wikipedia.org/wiki/Proxy_auto-config#The_PAC_File[proxy autoconfiguration file]. +Used with Safari as other proxy options do not work in Safari. + +To fine tune the proxy you use the original https://www.w3.org/TR/webdriver/#proxy[object] and pass it to capabilities: + +//:test-doc-blocks/skip +[source,clojure] +---- +(e/chrome {:capabilities + {:proxy + {:proxyType "manual" + :proxyAutoconfigUrl "some.proxy.com:8080" + :ftpProxy "some.proxy.com:8080" + :httpProxy "some.proxy.com:8080" + :noProxy ["http://this.url" "http://that.url"] + :sslProxy "some.proxy.com:8080" + :socksProxy "some.proxy.com:1080" + :socksVersion 5}}}) +---- +=== Connecting to an Existing Running WebDriver [[connecting-existing]] + +To connect to an existing WebDriver, specify the `:host` parameter. + +TIP: When neither the `:host` nor the `:webdriver-url` parameter is specified Etaoin will launch a new WebDriver process. + +The `:host` can be a hostname (localhost, some.remote.host.net) or an IP address (127.0.0.1, 183.102.156.31). +If the port is not specified, the <> `:port` is assumed. + +Both `:host` and `:port` are ignored if `:webdriver-url` is specified. + +Example: + +//:test-doc-blocks/skip +[source,clojure] +---- +;; Connect to an existing chromedriver process on localhost on port 9515 +(def driver (e/chrome {:host "127.0.0.1" :port 9515})) ;; for connection to driver on localhost on port 9515 + +;; Connect to an existing geckodriver process on remote most on default port +(def driver (e/firefox {:host "192.168.1.11"})) ;; the default port for firefox is 4444 + +;; Connect to a chrome instance on browserless.io via :webdriver-url +;; (replace YOUR-API-TOKEN with a valid browserless.io api token if you want to try this out) +(e2/with-chrome [driver + {:webdriver-url "https://chrome.browserless.io/webdriver" + :capabilities {"browserless:token" "YOUR-API-TOKEN" + "chromeOptions" {"args" ["--no-sandbox"]}}}] + (e/go driver "https://en.wikipedia.org/") + (e/wait-visible driver [{:id :simpleSearch} {:tag :input :name :search}]) + (e/fill driver {:tag :input :name :search} "Clojure programming language") + (e/fill driver {:tag :input :name :search} k/enter) + (e/get-title driver)) +;; => "Clojure programming language - Search results - Wikipedia" +---- + +=== Setting the Browser Profile [[browser-profile]] + +When running Chrome or Firefox, you may specify a special web browser profile made for test purposes. +A profile is a folder that keeps browser settings, history, bookmarks, and other user-specific data. + +Imagine, for example, that you'd like to run your integration tests against a user that turned off Javascript execution or image rendering. + +==== Create and Find a Profile in Chrome + +. In the right top corner of the main window, click on a user button. +. In the dropdown, select "Manage People". +. Click "Add person", submit a name and press "Save". +. The new browser window should appear. +Now, setup the new profile as you want. +. Open `chrome://version/` page. +Copy the file path that is beneath the `Profile Path` caption. + +==== Create and Find a Profile in Firefox + +. Run Firefox with `-P`, `-p` or `-ProfileManager` key as the https://support.mozilla.org/en-US/kb/profile-manager-create-and-remove-firefox-profiles[official page] describes. +. Create a new profile and run the browser. +. Setup the profile as you need. +. Open `about:support` page. +Near the `Profile Folder` caption, press the `Show in Finder` button. +A new folder window should appear. +Copy its path from there. + +==== Running a Driver with a Profile + +Once you've got a profile path, launch a driver with the `:profile` key as follows: + +//:test-doc-blocks/skip +[source,clojure] +---- +;; Chrome +(def chrome-profile + "/Users/ivan/Library/Application Support/Google/Chrome/Profile 2/Default") + +(def chrome-driver (e/chrome {:profile chrome-profile})) + +;; Firefox +(def ff-profile + "/Users/ivan/Library/Application Support/Firefox/Profiles/iy4iitbg.Test") + +(def firefox-driver (e/firefox {:profile ff-profile})) +---- + +== Writing Integration Tests For Your Application + +=== Basic Fixture + +It is desirable to have your tests be independent of one another. +One way to achieve this is through the use of a test fixture. +The fixture's job is to, for each test: + +1. create a new driver +2. run the test with the driver +3. shutdown the driver + +A dynamic `+*driver*+` var might be used to hold the driver. + +//:test-doc-blocks/skip +[source,clojure] +---- +(ns project.test.integration + "A module for integration tests" + (:require [clojure.test :refer [deftest is use-fixtures]] + [etaoin.api :as e])) + +(def ^:dynamic *driver*) + +(defn fixture-driver + "Executes a test running a driver. Bounds a driver + with the global *driver* variable." + [f] + (e/with-chrome [driver] + (binding [*driver* driver] + (f)))) + +(use-fixtures + :each ;; start and stop driver for each test + fixture-driver) + +;; now declare your tests + +(deftest ^:integration + test-some-case + (doto *driver* + (e/go url-project) + (e/click :some-button) + (e/refresh) + ... + )) +---- + +If for some reason you want to reuse a single driver instance for all tests: + +//:test-doc-blocks/skip +[source,clojure] +---- +(ns project.test.integration + "A module for integration tests" + (:require [clojure.test :refer [deftest is use-fixtures]] + [etaoin.api :as e] + [etaoin.api :as e2])) + +(def ^:dynamic *driver*) + +(defn fixture-browser [f] + (e2/with-chrome-headless [driver {:args ["--no-sandbox"]}] + (e/disconnect-driver driver) + (binding [*driver* driver] + (f)) + (e/connect-driver driver))) + +;; creating a session every time that automatically erases resources +(defn fixture-clear-browser [f] + (e/connect-driver *driver*) + (e/go *driver* "http://google.com") + (f) + (e/disconnect-driver *driver*)) + +;; this is run `once` before running the tests +(use-fixtures + :once + fixture-browser) + +;; this is run `every` time before each test +(use-fixtures + :each + fixture-clear-browser) + +...some tests +---- + +For faster testing you can use this example: + +//:test-doc-blocks/skip +[source,clojure] +---- +..... + +(defn fixture-browser [f] + (e2/with-chrome-headless [driver {:args ["--no-sandbox"]}] + (binding [*driver* driver] + (f)))) + +;; note that resources, such as cookies, are deleted manually, +;; so this does not guarantee that the tests are clean +(defn fixture-clear-browser [f] + (e/delete-cookies *driver*) + (e/go *driver* "http://google.com") + (f)) + +...... +---- + +=== Multi-Driver Fixtures + +In the example above, we examined a case when you run tests against a single type of driver. +However, you may want to test your site on multiple drivers, say, Chrome and Firefox. +In that case, your fixture may become a bit more complex: + +//:test-doc-blocks/skip +[source,clojure] +---- + +(def driver-type [:firefox :chrome]) + +(defn fixture-drivers [f] + (doseq [type driver-types] + (e/with-driver type {} driver + (binding [*driver* driver] + (testing (format "Testing in %s browser" (name type)) + (f)))))) +---- + +Now, each test will be run twice. +Once for Firefox and then once Chrome. +Please note the test call is prepended with the `testing` macro that puts the driver name into the report. +Once you've got an error, you'll easily find what driver failed the tests exactly. + +TIP: See also link:{url-tests}[Etaoin's API tests] for an example of this strategy. + +=== Postmortem Handler To Collect Artifacts + +To save some artifacts in case of an exception, wrap the body of your test into the `with-postmortem` handler as follows: + +//:test-doc-blocks/skip +[source,clojure] +---- +(deftest test-user-login + (e/with-postmortem *driver* {:dir "/path/to/folder"} + (doto *driver* + (e/go "http://127.0.0.1:8080") + (e/click-visible :login) + ;; any other actions... + ))) +---- + +If any exception occurs in that test, artifacts will be saved. + +To not copy and paste the options map, declare it at the top of the module. +If you use Circle CI, it would be great to save the data into a special artifacts directory that might be downloaded as a zip file once the build has been finished: + +//:test-doc-blocks/skip +[source,clojure] +---- +(def pm-dir + (or (System/getenv "CIRCLE_ARTIFACTS") ;; you are on CI + "/some/local/path")) ;; local machine + +(def pm-opt + {:dir pm-dir}) +---- + +Now pass that map everywhere into PM handler: + +//:test-doc-blocks/skip +[source,clojure] +---- + ;; test declaration + (e/with-postmortem *driver* pm-opt + ;; test body goes here + ) +---- + +Once an error occurs, you will find a PNG image that represents your browser page at the moment of exception and HTML dump. + +See <>. + +=== Running Tests By Tag + +Since UI tests may take lots of time to pass, it's definitely a good practice to pass both server and UI tests independently from each other. + +If you are using leiningen, here are a few tips. + +First, add `+^:integration+` tag to all the tests that are run under the browser like follows: + +//:test-doc-blocks/skip +[source,clojure] +---- +(deftest ^:integration + test-password-reset-pipeline + (doto *driver* + (go url-password-reset) + (click :reset-btn) + ;; and so on... + )) +---- + +Then, open your `project.clj` file and add test selectors: + +[source,clojure] +---- +:test-selectors {:default (complement :integration) + :integration :integration} +---- + +Now, when you launch `lein test` you will run all the tests except browser integration tests. +To run integration tests, launch `lein test :integration`. + +=== Check Whether a File has been Downloaded [[test-file-downloads]] + +Sometimes, a file starts to download automatically when you click on a link or just visit some page. +In tests, you might need to ensure a file really has been downloaded successfully. +A common scenario would be: + +* provide a custom empty download folder when running a browser (see <>). +* Click on a link or perform any action needed to start file downloading. +* Wait for some time; +for small files, 5-10 seconds would be enough. +* Using files API, scan that directory and try to find a new file. +Check if it matches a proper extension, name, creation date, etc. + +Example: + +//:test-doc-blocks/skip +[source,clojure] +---- +(require '[clojure.java.io :as io] + '[clojure.string :as str]) + +;; Local helper that checks whether it is really an Excel file. +(defn xlsx? [file] + (-> file + .getAbsolutePath + (str/ends-with? ".xlsx"))) + +;; Top-level declarations +(def DL-DIR "/Users/ivan/Desktop") +(def driver (e/chrome {:download-dir DL-DIR})) + +;; Later, in tests... +(e/click-visible driver :download-that-application) +(e/wait driver 7) ;; wait for a file has been downloaded + +;; Now, scan the directory and try to find a file: +(let [files (file-seq (io/file DL-DIR)) + found (some xlsx? files)] + (is found (format "No *.xlsx file found in %s directory." DL-DIR))) +---- + +== Running Selenium IDE files [[selenium-ide]] + +Etaoin can play the files produced by link:{ide}[Selenium IDE]. +Selenium IDE allows you to record web interactions for later playback. +It is installed as an optional extension in your web browser. + +Once installed, and activated, it records your actions into a JSON file with the `.side` extension. +You can save that file and run it with Etaoin. + +Let's imagine you've installed the IDE and recorded some actions as per Selenium IDE documentation. +Now that you have a `test.side` file, you could do something like this: + +//:test-doc-blocks/skip +[source,clojure] +---- +(require '[clojure.java.io :as io] + '[etaoin.api :as e] + '[etaoin.ide.flow :as flow]) + +(def driver (e/chrome)) + +(def ide-file (io/resource "ide/test.side")) + +(def opt + {;; The base URL redefines the one from the file. + ;; For example, the file was written on the local machine + ;; (http://localhost:8080), and we want to perform the scenario + ;; on staging (https://preprod-001.company.com) + :base-url "https://preprod-001.company.com" + + ;; keywords :test-.. and :suite-.. (id, ids, name, names) + ;; are used to select specific tests. When not passed, + ;; all tests get run. For example: + + :test-id "xxxx-xxxx..." ;; a single test by its UUID + :test-name "some-test" ;; a single test by its name + :test-ids ["xxxx-xxxx...", ...] ;; multiple tests by their ids + :test-names ["some-test1", ...] ;; multiple tests by their names + + ;; the same for suites: + + :suite-id ... + :suite-name ... + :suite-ids [...] + :suite-names [...]}) + +(flow/run-ide-script driver ide-file opt) +---- + +Everything related to the IDE feature can be found under the link:{url-doc}/CURRENT/api/etaoin.ide[etaoin.ide] namespace. + +=== CLI Arguments [[selenium-ide-cli]] + +You may also run a `.side` script from the command line. +Here is a `clojure` example: + +[source,shell] +---- +clojure -M -m etaoin.ide.main -d firefox -p '{:port 8888 :args ["--no-sandbox"]}' -r ide/test.side +---- + +As well as from an uberjar. +In this case, Etaoin must be in the primary dependencies, not the `:dev` or `:test` related. + +[source,shell] +---- +java -cp .../poject.jar -m etaoin.ide.main -d firefox -p '{:port 8888}' -f ide/test.side +---- + +We support the following arguments (check them out using the `clojure -M -m etaoin.ide.main -h` command): + +---- + -d, --driver-name name :chrome The name of driver. The default is `:chrome` + -p, --params params {} Parameters for the driver represented as an + EDN string, e.g '{:port 8080}' + -f, --file path Path to an IDE file on disk + -r, --resource path Path to an IDE resource + --test-ids ids Comma-separeted test ID(s) + --suite-ids ids Comma-separeted suite ID(s) + --test-names names Comma-separeted test name(s) + --suite-names names Comma-separeted suite name(s) + --base-url url Base URL for tests + -h, --help +---- + +Pay attention to `--params`. +This must be an EDN string representing a Clojure map. +That's the same map that you pass into a driver at creation time. + +Please note the IDE support is still experimental. +If you encounter unexpected behavior feel free to open an issue. +At the moment, we only support Chrome and Firefox for IDE files. + +== Webdriver in Docker + +To work with the driver in Docker, you can take ready-made images: + +Example for https://hub.docker.com/r/robcherry/docker-chromedriver/[Chrome]: + +[source,shell] +---- +docker run --name chromedriver -p 9515:4444 -d -e CHROMEDRIVER_WHITELISTED_IPS='' robcherry/docker-chromedriver:latest +---- + +for https://hub.docker.com/r/instrumentisto/geckodriver[Firefox]: + +[source,shell] +---- +docker run --name geckodriver -p 4444:4444 -d instrumentisto/geckodriver +---- + +To connect to an existing running WebDriver process you need to specify the `:host`. +In this example `:host` would be `localhost` or `127.0.0.1`. +The `:port` would be the appropirate port for the running WebDriver process as exposed by docker. +If the port is not specified, the <> port is set. + +//:test-doc-blocks/skip +[source,clojure] +---- +(def driver (e/chrome-headless {:host "localhost" :port 9515 :args ["--no-sandbox"]})) +(def driver (e/firefox-headless {:host "localhost"})) ;; will try to connect to port 4444 +---- + +== Troubleshooting [[troubleshooting]] + +=== Old Versions of WebDrivers can have Limitations + +[horizontal] +Reproduction:: For example, `chromedriver` used to throw an error when calling `maximize`: ++ +[source,clojure] +---- +(e2/with-chrome [driver] + (e/maximize driver)) +;; an exception with "cannot get automation extension" was thrown +---- +Cause:: This was a bug in `chromedriver` that was fixed in chromdriver v2.28. +Solution:: Updating to the current WebDriver resolved the issue. + +=== XPath and Searching from Root vs Current node + +Reproduction:: ++ +[source,clojure] +---- +;; we intend to find an element with the text 'some' under an element with id 'mishmash' +(e/get-element-text driver [{:id :mishmash} "//*[contains(text(),'some')]"]) +;; => "A little sample page to illustrate some concepts described in the Etaoin user guide." +;; but we've found the first element with text 'some' +---- +Cause:: In a vector, every expression searches from the previous one in a loop. +Without a leading dot, the XPath `+"//..."+` clause means to find an element from the root of the whole page. +With a dot, it means to find from the current node, which is one from the previous query, and so forth. +Solution:: Add the XPath dot. ++ +[source,clojure] +---- +(e/get-element-text driver [{:id :mishmash} ".//*[contains(text(),'some')]"]) +;; => "some other paragraph" +;; that's what we were looking for! +---- + +=== Clicking On Non-Visible Element + +Reproduction:: +//:test-doc-blocks/skip ++ +[source,clojure] +---- +(e/click driver :cantseeme) +;; as of this writing, on chrome throws an exception with message containing 'not interactable' +---- + +Cause:: You cannot interact with an element that is not visible or is so small that a human could not click on it. + +=== Selectors not Working + +Symptom:: Selectors for locating elements are not working, even though the elements are clearly available. + +Possible cause:: Your script may have clicked a link that opened a new tab or window. +Even though the new window is in the foreground, the driver instance is still connected to the original window. + +Solution:: Call `switch-window-next` when a new tab or window is opened to point the driver to the new tab/window. + +=== Unpredictable errors in Chrome when the window is not active + +Reproduction:: when you focus on another window, a WebDriver session that is run under Google Chrome fails. + +Solution:: Google Chrome may suspend a tab when it has been inactive for some time. +When the page is suspended, no operation can be done on it. +No clicks, Js execution, etc. +So try to keep Chrome window active during test session. + +=== Invalid argument: can't kill an exited process for Firefox + +Reproduction:: When you try to start the driver you get an error: +//:test-doc-blocks/skip ++ +[source,clojure] +---- +(def driver (e/firefox {:headless true})) +;; throws an exception containing message with 'invalid argument: can't kill an exited process' +---- + +Possible Cause:: Running Firefox as root in a regular user's session is not supported + +To Diagnose:: Run the driver with the path to the log files and the "trace" log level and explore the output. +//:test-doc-blocks/skip ++ +[source,clojure] +---- +(def driver (firefox {:log-stdout "ffout.log" :log-stderr "fferr.log" :driver-log-level "trace"})) +---- +Similar Problem:: https://github.com/mozilla/geckodriver/issues/1655 + +=== DevToolsActivePort file doesn't exist error on Chrome + +Reproduction:: When you try to start the chromedriver you get an error: + +//:test-doc-blocks/skip +[source,clojure] +---- +(def driver (e/chrome)) +;; throws an exception with message containing 'DevToolsActivePort file doesn't exist' +---- + +Possible Cause:: +A common cause for Chrome to crash during startup is running Chrome as root user (administrator) on Linux. +While it is possible to work around this issue by passing --no-sandbox flag when creating your WebDriver session, such a configuration is unsupported and highly discouraged. +You need to configure your environment to run Chrome as a regular user instead. + +Potential Solution:: Run driver with an argument `--no-sandbox`. +Caution! +This bypasses OS security model. ++ +[source,clojure] +---- +(e2/with-chrome [driver {:args ["--no-sandbox"]}] + (e/go driver "https://clojure.org")) +---- + +Similiar Problem:: A similar problem is described https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t[here] + +// A little invisible cheat to cleanup our driver +ifdef::env-test-doc-blocks[] +[source,clojure] +---- +(e/quit driver) +---- +endif::[] diff --git a/doc/02-developer-guide.adoc b/doc/02-developer-guide.adoc new file mode 100644 index 00000000..132e3139 --- /dev/null +++ b/doc/02-developer-guide.adoc @@ -0,0 +1,265 @@ += Developer Guide +:toclevels: 5 +:toc: + +== Contributing + +We very much appreciate contributions from the community. + +=== Issue First Please + +If you have an idea or a fix, please do raise a GitHub issue before investing in any coding effort. That way we can discuss first. +Writing code is the easy part, maintaining it forever is the hard part. + +That said, if you notice a simple typo, a PR without an issue is fine. + +=== Submitting a Pull Request + +The entire <> can take several minutes to run. +Depending on your change, you might choose to sanity test with a subset of tests or browsers. + +When you submit a PR, GitHub Actions will kick in and test across all supported browsers, OSes, Babashka and Clojure. +There's no shame in it finding a problem you didn't anticipate. +Given the nature of WebDrivers and browsers, it is not entirely unusual for a job or two to fail. +You can request GitHub Actions to rerun the failed jobs. +If they fail a second time you might have an issue to solve. + +== Environmental Overview + +=== Supported Environments + +Etaoin is tested on macOS, Ubuntu and Windows via GitHub Actions on each commit to the master branch. +All tests are run under Clojure and Babashka. +We test against against Chrome, Firefox, Edge and Safari xref:01-user-guide.adoc#supported-os-browser[depending on the OS]. + +=== Developer Prerequisites + +* Java Development Kit 1.8 or above +* Current version of Clojure cli for `clojure` command +** Note: the Etaoin library itself supports Clojure v1.9 and above +* Current vesion of Babashka +* Browsers and WebDrivers, see xref:01-user-guide.adoc#install-webdrivers[installation tips instructions in user guide] +** We currently test against is installed by GitHub Actions on their virtual environments. +They seem to keep browsers and drivers up to date. If we find we need to, we'll invest in tweaking these defaults, but we don't see a need as of this writing. +* ImageMagick - used by tests to verify that screenshots produce valid PNG files + +It is also useful to have access to the variety of OSes that Etaoin supports to diagnose and fix any OS-specific issues that may arise. + +=== Babashka Compatibility + +Etaoin is babashka compatible. + +Babashka supports everything that Etaoin needs, but when making changes, be aware that your code must also work under Babashka. For example, to make Etaoin Babashka compatible we made the following changes: + +1. Turf unused reference to `java.lang.IllegalThreadStateException` +2. Replace use of `org.clojure/data.codec` with JDK's `Base64` +3. Replace use of `ImageIO` in tests with a callout to ImageMagick instead. +4. Replace some JDK file related class references with `babashka/fs` abstractions +5. Use `http-client-lite` in place of `http-client` when running under Babashka +6. Run existing tests with cognitect test runner by including `babashka/tools.namespace` + +Nothing earth shattering there, but gives you and idea. + +== Docs + +All documentation is written in AsciiDoc. +@lread likes to follow https://asciidoctor.org/docs/asciidoc-recommended-practices/#one-sentence-per-line[AsciiDoc best practice of one sentence per line] but won't be entirely pedantic about that. + +We host our docs on cljdoc and have support for <> + + + + +== Babashka Tasks + +We use Babashka tasks, to see all available tasks run: + +[source,shell] +---- +bb tasks +---- + +=== Launching a REPL + +For a Clojure REPL +[source,shell] +---- +bb dev +---- + +For a babashka REPL +[source,shell] +---- +bb bb-dev +---- + +=== Checking Tools Versions + +Used by GitHub Actions, but also an interesting way to check your prerequisites: + +[source,shell] +---- +bb tools-versions +---- + +[[running-tests]] +=== Runing tests + +The `test` task provides a coarse grained facility to invoke tests. +It was written to satisfy the use case of running tests in parallel on GitHub Actions. + + +[source,shell] +---- +bb test --help +---- + +We'll likely add finer grained test selection to satisfy developer needs. +For now, temporarily tweak `./script/test.clj` if you need to. + +==== Testing User Guide Code Blocks + +There are many code examples in the user guide. +In an attempt to ensure they are in working order, we run a selection of them through https://github.com/lread/test-doc-blocks[test-doc-blocks]. + +[source,shell] +---- +bb test-doc +---- + +If you are updating the user guide, it preferable if your code block can be run through test-doc-blocks. +But if this is impractal, you can also have test-doc-blocks skip a code block. + +==== Testing within Docker + +If you wish, you can build a local docker image for testing on Linux. +You may want to try this because: + +* you are developing on macOS and want to run a sanity test on Linux +* or maybe you'd like to isolate a test run without windows popping up hither and thither (on docker we use a virtual display) + +To build a local docker image with Chrome and Firefox support: +[source,shell] +---- +bb docker-build +---- + +TIP: This will build a docker image with current releases of Chrome, Firefox and their respective WebDrivers. +Rerun the command as necessary. + +You can use this image interactively: +[source,shell] +---- +bb docker-run +bb test all +---- + +Or you can use it to run a single command: +[source,shell] +---- +bb docker-run bb test all +---- + +NOTE: `docker-run` plunks you automatically into `/etaoin` which maps to the etaoin project root + +There is nothing magic about the babashka docker tasks. +They are here for our convenience. +Feel free to run docker commands directly if that is more your thing. + +=== WebDriver Processes + +Sometimes WebDriver process might hang around longer than you'd like. + +To list them: +[source,shell] +---- +bb drivers +---- + +To terminate them: +[source,shell] +---- +bb drivers kill +---- + +=== Linting + +We use clj-kondo to lint Etaoin source code. + +To lint Etaoin sources: +[source,shell] +---- +bb lint +---- + +We like to keep our code free of lint warnings and fail CI if there are any lint issues. +This keeps our code tidy and also helps us to ensure our <> is working as expected. + +TIP: https://github.com/borkdude/clj-kondo/blob/master/doc/editor-integration.md[Integrate clj-kondo into your editor] to catch mistakes as you type them. + +=== Outdated dependencies + +To run check Etaoin dependencies: + +[source,shell] +---- +bb outdated +---- + +[[cljdoc-preview]] +=== Cljdoc Preview + +Before a release, it can be comforting to preview what docs will look like on https://cljdoc.org/[cljdoc]. + +[NOTE] +==== +This task should be considered experimental, I have only tested running on macOS, but am fairly confident it will work on Linux. +Not sure about Windows at this time. +==== + +[TIP] +==== +You have to push your changes to GitHub to preview them. This allows for a full preview that includes any links (source, images, etc) to GitHub. +This works fine from branches and forks. +==== + +Run `bb cljdoc-preview --help` for help. + +* `bb cljdoc-preview start` downloads (if necessary) and starts the cljdoc docker image +* `bb cljdoc-preview ingest` installs etaoin to your local maven repo and imports it into locally running cljdoc +* `bb cljdoc-preview view` opens a view to your imported docs in your default web browser +* `bb cljdoc-preview stop` stops the docker image + +== Other Notes + +=== Logging + +When running tests under the JVM, info level logging is configured via `env/test/resources/logback.xml`. This is automatically selected via the `:test` alias. You can prefix the `:debug` alias for debug level logging. See `script/test.clj` and tweak if necessary. + +For Babashka, logging levels are controlled via the built-in timbre library. +See `script/bb_test_runner.clj` and tweak if necessary. + +Sometimes tools like WireShark can also be helpful. +@lread personally used a combination of RawCap and WireShark on Windows to successfully diagnose an issue. + +[[clj-kondo-export]] +=== Clj-kondo Export Config + +Users of Etaoin and clj-kondo benefit from our clj-kondo export configuration. +As is the convention, you'll find it under `./resources/clj-kondo.exports/`. +We keep any clj-kondo config containing our local linting preferences, and pertinent to only our internal code, in `.clj-kondo/config.edn`. + +This configuration is included in the Etaoin release jar and available when folks reference Etaoin from their `deps.edn` form a `git` dependency. + +[NOTE] +==== +Etaoin contains a fair number of macros. +Clj-kondo can need special configuration (including hooks) to understand the effects of these macros. +So, when adding any new macros, think also about our Etaoin users and our clj-kondo export configuration. +==== + +== Useful References + +* https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/[chromedriver] +* https://github.com/mozilla/geckodriver[firefox geckodriver], https://searchfox.org/mozilla-central/source/testing/webdriver[sources] +* https://github.com/detro/ghostdriver/blob/[Phantom.js (obsolete, no longer tested)] diff --git a/doc/03-maintainer-guide.adoc b/doc/03-maintainer-guide.adoc new file mode 100644 index 00000000..0dabc582 --- /dev/null +++ b/doc/03-maintainer-guide.adoc @@ -0,0 +1,6 @@ += Maintainer Guide +:toclevels: 5 +:toc: + +== Introduction +Coming soon diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn new file mode 100644 index 00000000..eda1ecb0 --- /dev/null +++ b/doc/cljdoc.edn @@ -0,0 +1,6 @@ +{:cljdoc.doc/tree + [["Readme" {:file "README.adoc"}] + ["Changelog" {:file "CHANGELOG.adoc"}] + ["User Guide" {:file "doc/01-user-guide.adoc"}] + ["Developer Guide" {:file "doc/02-developer-guide.adoc"}]] + :cljdoc/languages ["clj"]} diff --git a/doc/user-guide-sample-frame1.html b/doc/user-guide-sample-frame1.html new file mode 100644 index 00000000..84ba07aa --- /dev/null +++ b/doc/user-guide-sample-frame1.html @@ -0,0 +1,2 @@ +

In frame1 paragraph

+ + +

A longer section

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed turpis tincidunt id aliquet risus feugiat in ante. Dui faucibus in ornare quam viverra. Turpis in eu mi bibendum neque egestas congue. Quam lacus suspendisse faucibus interdum posuere lorem ipsum. Nisl nunc mi ipsum faucibus vitae aliquet. Bibendum enim facilisis gravida neque convallis a cras semper. Vestibulum morbi blandit cursus risus at ultrices mi. Pellentesque diam volutpat commodo sed egestas. Id nibh tortor id aliquet lectus proin. Mauris nunc congue nisi vitae suscipit tellus. Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum. Pulvinar pellentesque habitant morbi tristique. At risus viverra adipiscing at in tellus integer feugiat scelerisque. Felis donec et odio pellentesque diam volutpat commodo sed. Neque sodales ut etiam sit amet nisl purus in. Nulla posuere sollicitudin aliquam ultrices sagittis orci a scelerisque. Lorem ipsum dolor sit amet consectetur adipiscing elit duis. + +Malesuada bibendum arcu vitae elementum. Nunc faucibus a pellentesque sit. Aliquet eget sit amet tellus cras. Pellentesque id nibh tortor id. Id nibh tortor id aliquet. Et molestie ac feugiat sed lectus vestibulum. Integer quis auctor elit sed vulputate mi sit amet. Metus aliquam eleifend mi in nulla posuere sollicitudin. Gravida in fermentum et sollicitudin ac orci phasellus egestas tellus. Sed felis eget velit aliquet sagittis id. Enim facilisis gravida neque convallis a cras semper auctor neque. Massa vitae tortor condimentum lacinia quis vel. + +Ut tortor pretium viverra suspendisse potenti nullam. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Mi in nulla posuere sollicitudin. Elit at imperdiet dui accumsan sit amet nulla. Urna nec tincidunt praesent semper feugiat nibh sed. Egestas purus viverra accumsan in. Aliquam sem fringilla ut morbi tincidunt augue. Commodo ullamcorper a lacus vestibulum sed arcu. In massa tempor nec feugiat nisl pretium fusce. Nibh mauris cursus mattis molestie a. Nunc consequat interdum varius sit. In est ante in nibh. Augue interdum velit euismod in pellentesque massa placerat. Tincidunt id aliquet risus feugiat in ante. Ac turpis egestas integer eget aliquet nibh praesent tristique magna. Quam adipiscing vitae proin sagittis nisl. Tellus id interdum velit laoreet id donec ultrices tincidunt. + +Molestie at elementum eu facilisis sed odio morbi. Mauris rhoncus aenean vel elit scelerisque mauris. Maecenas volutpat blandit aliquam etiam. Amet consectetur adipiscing elit pellentesque habitant morbi tristique senectus et. Urna et pharetra pharetra massa massa ultricies mi. Turpis egestas maecenas pharetra convallis posuere morbi leo urna molestie. Sed viverra tellus in hac habitasse. Gravida dictum fusce ut placerat orci. Tellus pellentesque eu tincidunt tortor aliquam nulla facilisi cras. Facilisis mauris sit amet massa vitae. In hendrerit gravida rutrum quisque. + +Sed vulputate odio ut enim blandit. Vulputate eu scelerisque felis imperdiet proin fermentum leo. Et malesuada fames ac turpis egestas integer eget. Ante in nibh mauris cursus mattis molestie a iaculis. Bibendum neque egestas congue quisque egestas diam in arcu. Sed adipiscing diam donec adipiscing tristique. Non consectetur a erat nam at lectus. Orci nulla pellentesque dignissim enim. Velit ut tortor pretium viverra suspendisse potenti nullam. Dolor morbi non arcu risus quis. Nulla facilisi etiam dignissim diam quis enim. Dolor morbi non arcu risus quis varius quam. Nunc sed blandit libero volutpat sed cras ornare. Libero volutpat sed cras ornare arcu. Scelerisque varius morbi enim nunc faucibus a. Euismod in pellentesque massa placerat. Scelerisque in dictum non consectetur. + +Neque gravida in fermentum et sollicitudin. Vel orci porta non pulvinar neque laoreet suspendisse interdum consectetur. Risus feugiat in ante metus. Cursus in hac habitasse platea dictumst quisque sagittis. Dolor morbi non arcu risus quis varius quam. Habitasse platea dictumst vestibulum rhoncus est pellentesque elit. In hac habitasse platea dictumst. Porttitor eget dolor morbi non arcu. Nunc mattis enim ut tellus elementum sagittis vitae et. Accumsan sit amet nulla facilisi morbi tempus. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt. Feugiat pretium nibh ipsum consequat nisl vel pretium. Ut tristique et egestas quis ipsum suspendisse ultrices gravida. + +Aliquam sem fringilla ut morbi tincidunt augue interdum. Fermentum dui faucibus in ornare quam viverra orci. Donec ac odio tempor orci dapibus ultrices in. Volutpat commodo sed egestas egestas. Rhoncus est pellentesque elit ullamcorper dignissim. Aliquam eleifend mi in nulla posuere. Vitae elementum curabitur vitae nunc sed velit. Proin sed libero enim sed faucibus turpis. Id ornare arcu odio ut. Proin nibh nisl condimentum id venenatis a. Donec pretium vulputate sapien nec sagittis aliquam. Eu lobortis elementum nibh tellus molestie nunc non blandit massa. Sagittis purus sit amet volutpat consequat mauris nunc. Eget sit amet tellus cras adipiscing. Curabitur vitae nunc sed velit. Risus pretium quam vulputate dignissim suspendisse in est ante. Aenean pharetra magna ac placerat. Gravida arcu ac tortor dignissim convallis aenean et tortor. Nulla aliquet porttitor lacus luctus accumsan tortor posuere ac ut. + +Lectus quam id leo in vitae turpis massa. Etiam non quam lacus suspendisse. Gravida quis blandit turpis cursus in hac. Urna neque viverra justo nec ultrices dui. Nec feugiat in fermentum posuere. Elit at imperdiet dui accumsan. Quis eleifend quam adipiscing vitae proin. Integer quis auctor elit sed vulputate mi sit. Quam id leo in vitae turpis massa sed. Sagittis aliquam malesuada bibendum arcu vitae. Tincidunt eget nullam non nisi est sit amet facilisis magna. Nunc mi ipsum faucibus vitae aliquet. + +Feugiat scelerisque varius morbi enim nunc faucibus a pellentesque. Aenean sed adipiscing diam donec adipiscing tristique risus nec feugiat. Ultricies leo integer malesuada nunc vel risus commodo viverra. Urna neque viverra justo nec. Interdum velit euismod in pellentesque massa. Sed cras ornare arcu dui vivamus arcu felis. Risus quis varius quam quisque id. Cursus metus aliquam eleifend mi in. Eget felis eget nunc lobortis mattis aliquam faucibus purus in. Nunc scelerisque viverra mauris in aliquam sem fringilla ut. Morbi tincidunt ornare massa eget egestas purus. Nisl vel pretium lectus quam id leo. + +Enim praesent elementum facilisis leo. Placerat in egestas erat imperdiet sed euismod nisi. Volutpat sed cras ornare arcu dui vivamus arcu felis bibendum. Erat imperdiet sed euismod nisi. Mauris commodo quis imperdiet massa tincidunt nunc pulvinar sapien et. Ultrices mi tempus imperdiet nulla malesuada pellentesque elit eget. Pretium vulputate sapien nec sagittis aliquam. Arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque. Amet consectetur adipiscing elit duis. Arcu cursus vitae congue mauris. Volutpat maecenas volutpat blandit aliquam. At ultrices mi tempus imperdiet nulla malesuada pellentesque elit. Purus non enim praesent elementum facilisis leo vel fringilla est. Massa eget egestas purus viverra accumsan in nisl nisi. +

Last section

+
+   Avast, me proud beauty! Wanna know why my Roger is so Jolly? You’re drinking a Salty Dog? How’d you like to try the real thing? Drink up me hearties yoho …a pirates life for me Always be yourself, unless you can be a pirate. Then always be a pirate. STOP BLOWING HOLES IN MY SHIP!!! Work like a captain, play like a pirate. Arrrrrrrr Ahoy! lets trouble the water!
+
+   Come on up and see me urchins. Yes, that is a hornpipe in my pocket and I am happy to see you. Why is the rum gone? The average man will bristle if you say his father was dishonest, but he will brag a little if he discovers that his great- grandfather was a pirate.
+
+   You can always trust the untrustworthy because you can always trust that they will be untrustworthy. Its the trustworthy you can’t trust. Not all treasure is silver and gold Yarrrr! there be ony two ranks of leader amongst us pirates! Captain and if your really notorious then it’s Cap’n! Whats a pirate’s favorite fast food restaurant?  Arrrrbys! Arrrrrrrr
+
+   That’s some treasure chest you’ve got there. What are YOU doing here? Why is the rum gone? Drink up me hearties yoho …a pirates life for me Suddenly you’re like a pirate, you’re 65 years old and you’ve got an ear- ring. Work like a captain, play like a pirate. Well actualy piracy is a democracy with captains voted for by the crew. Arrrrrrrr
+
+   Have ya ever met a man with a real yardarm? You’re drinking a Salty Dog? How’d you like to try the real thing? So, tell me, why do they call ye, “Cap’n Feathersword?” You can always trust the untrustworthy because you can always trust that they will be untrustworthy. Its the trustworthy you can’t trust.
+   
+ + diff --git a/env/dev/demo.clj b/env/dev/demo.clj index 93efdaae..dcb0617c 100644 --- a/env/dev/demo.clj +++ b/env/dev/demo.clj @@ -1,45 +1,44 @@ +(require '[etaoin.api :as e] + '[etaoin.keys :as k]) -(use 'etaoin.api) -(require '[etaoin.keys :as k]) +(def driver (e/chrome)) -(def driver (chrome)) - -(go driver "https://en.wikipedia.org/") +(e/go driver "https://en.wikipedia.org/") (def query-search {:tag :input :name :search}) -(wait-visible driver [{:id :simpleSearch} query-search]) +(e/wait-visible driver [{:id :simpleSearch} query-search]) ;; search for something -(fill driver query-search "Clojure programming language") +(e/fill driver query-search "Clojure programming language") -(clear driver query-search) +(e/clear driver query-search) -(fill-human driver query-search "Clojure programming language") +(e/fill-human driver query-search "Clojure programming language") -(fill driver query-search k/enter) -(wait-visible driver {:class :mw-search-results}) +(e/fill driver query-search k/enter) +(e/wait-visible driver {:class :mw-search-results}) -(scroll-down driver 100) +(e/scroll-down driver 100) ;; I'm sure the first link is what I was looking for -(click driver [{:class :mw-search-results} - {:class :mw-search-result-heading} - {:tag :a}]) +(e/click driver [{:class :mw-search-results} + {:class :mw-search-result-heading} + {:tag :a}]) -(wait-visible driver {:id :firstHeading}) +(e/wait-visible driver {:id :firstHeading}) -(get-url driver) -(get-title driver) +(e/get-url driver) +(e/get-title driver) -(has-text? driver "Clojure") +(e/has-text? driver "Clojure") ;; navigate on history -(back driver) -(forward driver) -(refresh driver) +(e/back driver) +(e/forward driver) +(e/refresh driver) -(screenshot driver "clojure.png") +(e/screenshot driver "clojure.png") ;; stops Firefox and HTTP server -(quit driver) +(e/quit driver) diff --git a/env/dev/resources/log4j.properties b/env/dev/resources/log4j.properties deleted file mode 100644 index 65be0752..00000000 --- a/env/dev/resources/log4j.properties +++ /dev/null @@ -1,5 +0,0 @@ -log4j.rootLogger=INFO, console -log4j.logger.etaoin=DEBUG -log4j.appender.console=org.apache.log4j.ConsoleAppender -log4j.appender.console.layout=org.apache.log4j.PatternLayout -log4j.appender.console.layout.ConversionPattern=%-5p %c: %m%n diff --git a/env/dev/resources/logback.xml b/env/dev/resources/logback.xml new file mode 100644 index 00000000..859917a1 --- /dev/null +++ b/env/dev/resources/logback.xml @@ -0,0 +1,24 @@ + + + + + + + %date %-5level %logger{36} - %message%n + + + + + + + + + + + + + + + diff --git a/resources/html/drag-n-drop/example.css b/env/test/resources/html/drag-n-drop/example.css similarity index 100% rename from resources/html/drag-n-drop/example.css rename to env/test/resources/html/drag-n-drop/example.css diff --git a/resources/html/drag-n-drop/example.dart.js b/env/test/resources/html/drag-n-drop/example.dart.js similarity index 100% rename from resources/html/drag-n-drop/example.dart.js rename to env/test/resources/html/drag-n-drop/example.dart.js diff --git a/resources/html/drag-n-drop/images/README.md b/env/test/resources/html/drag-n-drop/images/README.md similarity index 100% rename from resources/html/drag-n-drop/images/README.md rename to env/test/resources/html/drag-n-drop/images/README.md diff --git a/resources/html/drag-n-drop/images/document.png b/env/test/resources/html/drag-n-drop/images/document.png similarity index 100% rename from resources/html/drag-n-drop/images/document.png rename to env/test/resources/html/drag-n-drop/images/document.png diff --git a/resources/html/drag-n-drop/images/trash.png b/env/test/resources/html/drag-n-drop/images/trash.png similarity index 100% rename from resources/html/drag-n-drop/images/trash.png rename to env/test/resources/html/drag-n-drop/images/trash.png diff --git a/resources/html/drag-n-drop/index.html b/env/test/resources/html/drag-n-drop/index.html similarity index 100% rename from resources/html/drag-n-drop/index.html rename to env/test/resources/html/drag-n-drop/index.html diff --git a/resources/html/simple.html b/env/test/resources/html/simple.html similarity index 100% rename from resources/html/simple.html rename to env/test/resources/html/simple.html diff --git a/resources/html/test.html b/env/test/resources/html/test.html similarity index 97% rename from resources/html/test.html rename to env/test/resources/html/test.html index 1e4473e5..12aa10de 100644 --- a/resources/html/test.html +++ b/env/test/resources/html/test.html @@ -50,7 +50,8 @@

Select section

- + +