├── src ├── Emakefile ├── webdrv_cap.erl ├── json.erl ├── webdrv_wire.erl └── webdrv_session.erl ├── test ├── Emakefile ├── webdrv_http_srv.erl ├── webdrv_misc_test.erl └── webdrv_eqc.erl ├── ebin ├── Emakefile └── webdrv.app ├── .gitignore ├── data ├── page0.html ├── page1.html ├── page2.html ├── windows.html └── elements.html ├── Makefile.in ├── configure.in ├── LICENSE ├── README.md ├── doc └── overview.edoc └── include └── webdrv.hrl /src/Emakefile: -------------------------------------------------------------------------------- 1 | {'*', [{outdir, "../ebin"}]}. 2 | -------------------------------------------------------------------------------- /test/Emakefile: -------------------------------------------------------------------------------- 1 | {'*', [{outdir, "../ebin"}]}. 2 | -------------------------------------------------------------------------------- /ebin/Emakefile: -------------------------------------------------------------------------------- 1 | {'../src/*', []}. 2 | {'../test/*', []}. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | .eqc-info 7 | current_counterexample.eqc 8 | -------------------------------------------------------------------------------- /data/page0.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

A simple test page0!

4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /data/page1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

A simple test page1!

4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /data/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

A simple test page2!

4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ebin/webdrv.app: -------------------------------------------------------------------------------- 1 | {application, webdrv, [ 2 | {description, "WebDriver implementation in Erlang"}, 3 | {modules, [json, webdrv_cap, webdrv_session, webdrv_wire]}, 4 | {applications, [kernel, stdlib]}, 5 | {vsn, "1.0.0"} 6 | ]}. 7 | -------------------------------------------------------------------------------- /data/windows.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Open window/tab named WINDOW0
4 | Open window/tab named WINDOW1
5 | Open window/tab named WINDOW2
6 | 7 | 8 | -------------------------------------------------------------------------------- /data/elements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /Makefile.in: -------------------------------------------------------------------------------- 1 | PREFIX=@prefix@ 2 | VERSION=1.0 3 | RELDIR=./rel/webdrv-$(VERSION) 4 | ERL=@WithErl@ 5 | 6 | all: compile doc 7 | .PHONY: test doc 8 | 9 | compile: 10 | @(cd src; $(ERL) -make) 11 | 12 | test: 13 | ifeq (@ERLANG_LIB_VER_eqc@, not found) 14 | @(echo "Cannot test without QuickCheck installed.") 15 | else 16 | @(cd test; $(ERL) -make) 17 | endif 18 | 19 | doc: 20 | erl -eval "edoc:application(webdrv,\".\",[{dir, \"./doc\"}, {preprocess, true}])" \ 21 | -s init stop -noshell 22 | 23 | release: compile 24 | rm -rf $(RELDIR);\ 25 | mkdir -p $(RELDIR);\ 26 | cp -r ebin doc include $(RELDIR) 27 | 28 | install: release 29 | mkdir -p $(PREFIX) 30 | cp -r $(RELDIR) $(PREFIX)/ 31 | 32 | clean: 33 | rm -f ebin/*.beam; \ 34 | rm -f doc/*.html; \ 35 | rm -f doc/*.png doc/*info doc/*.css; \ 36 | rm -rf rel 37 | -------------------------------------------------------------------------------- /configure.in: -------------------------------------------------------------------------------- 1 | dnl 2 | dnl webdrv configure script 3 | dnl 4 | 5 | AC_INIT(Makefile.in) 6 | 7 | dnl check if eqc is installed 8 | AC_ERLANG_CHECK_LIB([eqc], 9 | [], 10 | [AC_MSG_WARN([QuickCheck not found.])]) 11 | 12 | dnl allow a different Erlang compiler to be specified 13 | AC_ARG_WITH(erl, 14 | [ --with-erl= 15 | Use a command different from 'erl' to compile Erlang code. 16 | ], 17 | [WithErl="$withval"], 18 | [WithErl=$ERL] 19 | ) 20 | AC_SUBST(WithErl) 21 | 22 | if test "$WithErl" = "" -o "$WithErl" = "no"; then 23 | AC_MSG_ERROR([Cannot continue without Erlang compiler!]) 24 | fi 25 | 26 | if test "$WithErl" != "$ERL"; then 27 | AC_MSG_WARN([Using $WithErl as Erlang compiler.]) 28 | fi 29 | 30 | AC_PREFIX_DEFAULT(/usr/local/lib/erlang/lib) 31 | 32 | dnl read Makefile.in and write Makefile 33 | AC_OUTPUT(Makefile) 34 | 35 | dnl How to continue 36 | echo "****************************************************" 37 | echo "Configuration done." 38 | echo "Type \"make\" in order to build webdrv, then," 39 | echo "optionally, \"make install\" to install webdrv." 40 | echo "****************************************************" 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person 2 | obtaining a copy of this software and associated documentation 3 | files (the "Software"), to deal in the Software without 4 | restriction, including without limitation the rights to use, copy, 5 | modify, merge, publish, distribute, sublicense, and/or sell copies 6 | of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The software MAY NOT be used in combination with the Proper tool. 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 18 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 19 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/webdrv_http_srv.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Hans Svensson 3 | %%% @copyright (C) 2013, Quviq AB 4 | %%% @license 5 | %%% Permission is hereby granted, free of charge, to any person 6 | %%% obtaining a copy of this software and associated documentation 7 | %%% files (the "Software"), to deal in the Software without 8 | %%% restriction, including without limitation the rights to use, copy, 9 | %%% modify, merge, publish, distribute, sublicense, and/or sell copies 10 | %%% of the Software, and to permit persons to whom the Software is 11 | %%% furnished to do so, subject to the following conditions: 12 | %%% 13 | %%% The above copyright notice and this permission notice shall be 14 | %%% included in all copies or substantial portions of the Software. 15 | %%% 16 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | %%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 20 | %%% BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 21 | %%% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | %%% CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | %%% SOFTWARE. 24 | %%%------------------------------------------------------------------- 25 | %%% @doc 26 | %%% Small http server - should be able to serve static pages just for the purpouse of 27 | %%% testing WebDriver. 28 | %%% @end 29 | %%%------------------------------------------------------------------- 30 | 31 | -module(webdrv_http_srv). 32 | 33 | -export([start/0, stop/0, stop/1]). 34 | 35 | conf() -> 36 | [{port, 8088}, 37 | {server_root, "../"}, 38 | {document_root, "../data"}, 39 | {server_name, "localhost"}]. 40 | 41 | start() -> 42 | inets:start(), 43 | {ok, Pid} = inets:start(httpd, conf()), 44 | Pid. 45 | 46 | stop(ServerPid) -> 47 | ok = inets:stop(httpd, ServerPid). 48 | 49 | stop() -> 50 | ok = inets:stop(httpd, {{127,0,0,1}, 8088}). 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | webdrv - WebDriver implementation in Erlang 2 | =========================================== 3 | 4 | This package contains an implementation of the WebDriver API in Erlang. The WebDriver API 5 | provides a platform- and language-neutral interface that allows programs to control the 6 | behavior and inspect the state of a web browser. It is mainly intended for automated 7 | tests, but can also be used to control a browser for other purposes. 8 | 9 | The WebDriver API is accessed via http-requests, data is serialized using JSON. 10 | 11 | This package contains two levels; one purerly functional level, where separate WebDriver 12 | commands can be made, and a gen_server based level, wrapping the concept of a WebDriver 13 | session (i.e. an interaction with a browser instance). 14 | 15 | The WebDriver API is currently being standardized by W3C (the latest working draft when 16 | writing this is from May 30th 2013), however the main implementors of the server-side of 17 | WebDriver (like Selenium and ChromeDriver) still follow the protocol as described by 18 | Selenium -- 19 | [The WebDriver Wire Protocol](https://code.google.com/p/selenium/wiki/JsonWireProtocol). 20 | 21 | This implementation spans most of what is covered by the Wire Protocol, with the exception 22 | of the Touch interface, Geographical location and local storage. Extending with these 23 | things should be straightforward but we have not seen a usage of this functionality yet. 24 | 25 | The package also contains tests, in the form of a [QuickCheck](http://quviq.com/) model, 26 | testing both the session wrapper and the Wire Protocol. Page navigation, window control 27 | and element location is well tested, while page element interaction and cookies remains as 28 | TODO. 29 | 30 | Building and Installing 31 | ----------------------- 32 | 33 | There is a simplistic `Makefile` included in the repository. The command: 34 | 35 | make all 36 | 37 | compiles all sources to BEAM's, and builds the documentation. `make all` is short for 38 | `make compile` and `make doc`. The command: 39 | 40 | make test 41 | 42 | compiles all tests (to `./ebin`). The command: 43 | 44 | make release 45 | 46 | creates a ready-to-be-installed directory in `./rel/webdrv-VERSION`. There is no automagic 47 | installation, if you want the library permanently in your code path; copy the generated 48 | directory to your `$ERLANG/lib/`. Finally, the command: 49 | 50 | make clean 51 | 52 | removes all(?) generated files from the project. 53 | 54 | Contributing 55 | ------------ 56 | 57 | Please don't hesitate to improve and/or extend the implementation or the tests, there is 58 | plenty of room for improvement. The easiest way to contribute is: 59 | 60 | 1. Fork it. 61 | 2. Create a branch (`git checkout -b my_webdriver`) 62 | 3. Commit your changes (`git commit -am "Added new cool feature X"`) 63 | 4. Push to the branch (`git push origin my_webdriver`) 64 | 5. Open a [Pull Request][1] 65 | 66 | [1]: http://github.com/Quviq/webdrv/pulls 67 | -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------ 2 | @author Hans Svensson 3 | @copyright (C) 2013, Quviq AB 4 | 5 | @title WebDriver implementation for Erlang 6 | 7 | @doc 8 | 9 | This package contains an implementation of the WebDriver API in Erlang. The WebDriver API 10 | provides a platform- and language-neutral interface that allows programs to control the 11 | behavior and inspect the state of a web browser. It is mainly intended for automated 12 | tests, but can also be used to control a browser for other purposes. 13 | 14 | The WebDriver API is accessed via http-requests, data is serialized using JSON. 15 | 16 | This package contains two levels; one purerly functional level, where separate WebDriver 17 | commands can be made, and a gen_server based level, wrapping the concept of a WebDriver 18 | session (i.e. an interaction with a browser instance). 19 | 20 | The WebDriver API is currently being standardized by W3C (the latest working draft when 21 | writing this is from May 30th 2013), however the main implementors of the server-side of 22 | WebDriver (like Selenium and ChromeDriver) still follow the protocol as described by 23 | Selenium (The WebDriver 24 | Wire Protocol). 25 | 26 | This implementation spans most of what is covered by the Wire Protocol, with the exception 27 | of the Touch interface, Geographical location and local storage. Extending with these 28 | things should be straightforward but we have not seen a usage of this functionality yet. 29 | 30 | The package also contains tests, in the form of a QuickCheck model, testing both the session wrapper and the 32 | Wire Protocol. Page navigation, window control and element location is well tested, while 33 | page element interaction and cookies remains as TODO. 34 | 35 |

License

36 |
37 | Permission is hereby granted, free of charge, to any person
38 | obtaining a copy of this software and associated documentation
39 | files (the "Software"), to deal in the Software without
40 | restriction, including without limitation the rights to use, copy,
41 | modify, merge, publish, distribute, sublicense, and/or sell copies
42 | of the Software, and to permit persons to whom the Software is
43 | furnished to do so, subject to the following conditions:
44 | 
45 | The above copyright notice and this permission notice shall be
46 | included in all copies or substantial portions of the Software.
47 | 
48 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
49 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
50 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
51 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
52 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
53 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
54 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
55 | SOFTWARE. 
56 | @end 57 | ------------------------------------------------------------------ 58 | -------------------------------------------------------------------------------- /include/webdrv.hrl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Hans Svensson 3 | %%% @copyright (C) 2013, Quviq AB 4 | %%% @license 5 | %%% Permission is hereby granted, free of charge, to any person 6 | %%% obtaining a copy of this software and associated documentation 7 | %%% files (the "Software"), to deal in the Software without 8 | %%% restriction, including without limitation the rights to use, copy, 9 | %%% modify, merge, publish, distribute, sublicense, and/or sell copies 10 | %%% of the Software, and to permit persons to whom the Software is 11 | %%% furnished to do so, subject to the following conditions: 12 | %%% 13 | %%% The above copyright notice and this permission notice shall be 14 | %%% included in all copies or substantial portions of the Software. 15 | %%% 16 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | %%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 20 | %%% BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 21 | %%% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | %%% CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | %%% SOFTWARE. 24 | %%%------------------------------------------------------------------- 25 | %%% @doc 26 | %%% Includes for WebDriver 27 | %%% @end 28 | %%%------------------------------------------------------------------- 29 | 30 | -record(capability, 31 | { browserName 32 | %% , browserVersion 33 | , version = <<"">> 34 | %% , platformName 35 | , platform = <<"ANY">> 36 | %% , platformVersion 37 | , javascriptEnabled = true 38 | , device = <<"">> 39 | }). 40 | 41 | -record(webdrv_opts, { url, timeout = 5000, session_id = null }). 42 | 43 | -type session_id() :: string() | null. 44 | -type url() :: string(). 45 | 46 | -type frame_id() :: jsonstr() | number() | null. 47 | 48 | -type json() :: jsonobj() | jsonlist() | jsonnum() | jsonstr() | true | false | null. 49 | -type jsonobj() :: {obj, [{jsonkey(), json()}]}. 50 | -type jsonkey() :: string(). 51 | -type jsonlist() :: [json()]. 52 | -type jsonnum() :: integer() | float(). 53 | -type jsonstr() :: binary(). 54 | 55 | -type request_error() :: {html_error, string()} 56 | | {json_error, string()} 57 | | {cmd_error, json()} 58 | | {wire_error, {atom(), string()}, session_id()} 59 | | {type_error, atom(), term()}. 60 | 61 | -type request_ok() :: {ok, session_id(), jsonobj()}. 62 | 63 | -type request_res() :: request_ok() | request_error(). 64 | 65 | -type capability() :: #capability{} | null | jsonobj(). 66 | 67 | -type orientation() :: landscape | portrait. 68 | -type cookie() :: jsonobj(). 69 | 70 | -type button() :: integer() | left | middle | right. 71 | 72 | -type log_entry() :: {number(), string(), string()}. 73 | -type log() :: [log_entry()]. 74 | 75 | -type cache_status() :: 76 | uncached | idle | checking | downloading | update_ready | obsolete. 77 | 78 | -------------------------------------------------------------------------------- /src/webdrv_cap.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Hans Svensson 3 | %%% @copyright (C) 2013, Quviq AB 4 | %%% 5 | %%% @doc 6 | %%% Handling of capabilities for WebDriver protocol 7 | %%% 8 | %%%

License

9 | %%%
10 | %%% Permission is hereby granted, free of charge, to any person
11 | %%% obtaining a copy of this software and associated documentation
12 | %%% files (the "Software"), to deal in the Software without
13 | %%% restriction, including without limitation the rights to use, copy,
14 | %%% modify, merge, publish, distribute, sublicense, and/or sell copies
15 | %%% of the Software, and to permit persons to whom the Software is
16 | %%% furnished to do so, subject to the following conditions:
17 | %%%
18 | %%% The above copyright notice and this permission notice shall be
19 | %%% included in all copies or substantial portions of the Software.
20 | %%%
21 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 | %%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
25 | %%% BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
26 | %%% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
27 | %%% CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 | %%% SOFTWARE. 
29 | %%% @end 30 | %%%------------------------------------------------------------------- 31 | 32 | -module(webdrv_cap). 33 | 34 | -include("../include/webdrv.hrl"). 35 | 36 | -export([default/0, default_browser/1, 37 | to_json/1, 38 | default_firefox/0, default_chrome/0, default_htmlunit/0, 39 | default_safari/1]). 40 | 41 | %% @doc Return the default capability record. 42 | -spec default() -> #capability{}. 43 | default() -> 44 | #capability{ }. 45 | 46 | %% @hidden Just for the tests 47 | -spec default_browser(string() | atom()) -> #capability{}. 48 | default_browser(Browser) when is_atom(Browser) -> 49 | (default())#capability{ browserName = list_to_binary(atom_to_list(Browser))}; 50 | default_browser(Browser) when is_list(Browser) -> 51 | (default())#capability{ browserName = list_to_binary(Browser)}. 52 | 53 | %% @doc Return the default capability with browser set to chrome. 54 | -spec default_chrome() -> #capability{}. 55 | default_chrome() -> 56 | (default())#capability{ browserName = <<"chrome">>, version = <<"">> }. 57 | 58 | %% @doc Return the default capability with browser set to htmlunit. 59 | -spec default_htmlunit() -> #capability{}. 60 | default_htmlunit() -> 61 | (default())#capability{ browserName = <<"htmlunit">>, version = <<"">> }. 62 | 63 | %% @doc Return the default capability with browser set to firefox. 64 | -spec default_firefox() -> #capability{}. 65 | default_firefox() -> 66 | (default())#capability{ browserName = <<"firefox">> }. 67 | 68 | %% @doc Return the default capability with browser set to firefox. 69 | -spec default_safari(string()) -> #capability{}. 70 | default_safari(Device) -> 71 | (default())#capability{ browserName = <<"Safari">>, device = Device }. 72 | 73 | %% @doc Convert a capability (possibly null) to JSON format. Function is 74 | %% idempotent, i.e. converting an already converted object is fine. 75 | -spec to_json(capability()) -> jsonobj() | null. 76 | to_json(C = #capability{}) -> 77 | {obj, 78 | [ {Field, Arg1} || {Field, Arg1, Arg2} <- lists:zip3(record_info(fields, capability), 79 | tl(tuple_to_list(C)), 80 | tl(tuple_to_list(#capability{}))), 81 | (Arg1 =/= Arg2 orelse Field == javascriptEnabled)]}; 82 | to_json(Obj = {obj, _Any}) -> Obj; 83 | to_json(null) -> null. 84 | 85 | -------------------------------------------------------------------------------- /test/webdrv_misc_test.erl: -------------------------------------------------------------------------------- 1 | %%% @author Hans Svensson <> 2 | %%% @copyright (C) 2012, Hans Svensson 3 | %%% @doc 4 | %%% Webdriver implementation in Erlang 5 | %%% @end 6 | %%% Created : 20 Dec 2012 by Hans Svensson <> 7 | 8 | -module(webdrv_misc_test). 9 | 10 | -compile(export_all). 11 | -include("../include/webdrv.hrl"). 12 | -define(SELENIUM, "http://localhost:4444/wd/hub/"). 13 | -define(CHROMEDRIVER, "http://localhost:9515/"). 14 | 15 | relative_mess() -> 16 | {ok, _Pid} = webdrv_session:start_session(test, ?SELENIUM, webdrv_cap:default_htmlunit(), 10000), 17 | webdrv_session:set_url(test, "http://localhost:8088/elements.html"), 18 | ElemSelects = [ X || {X, _} <- all_elements() ], 19 | ElemSelects2 = [ X || {X, _} <- all_elements2() ], 20 | %% Test that we can really find all of these 21 | Elems = lists:map(fun({St, V}) -> {ok, E} = webdrv_session:find_element(test, St, V), E end, 22 | ElemSelects), 23 | io:format("Elems: ~p\n", [lists:zip(ElemSelects, Elems)]), 24 | Res = [ {E1, E2, find_rel(E,E2)} || {E, E1} <- lists:zip(Elems, ElemSelects), 25 | E2 <- ElemSelects2], 26 | io:format("\nRes:\n~p\n", [Res]), 27 | webdrv_session:stop_session(test). 28 | 29 | find_rel(E, {St, V}) -> 30 | case webdrv_session:find_element_rel(test, E, St, V) of 31 | {ok, _} -> io:format("."), ok; 32 | _ -> io:format("x"), fail 33 | end. 34 | 35 | open_window(Session, Window) -> 36 | webdrv_session:set_url(Session, "http://localhost:8088/windows.html"), 37 | {ok, E} = webdrv_session:find_element(Session, "name", Window), 38 | webdrv_session:click_element(Session, E), 39 | webdrv_session:set_window_focus(Session, Window). 40 | 41 | test5() -> 42 | R1 = webdrv_wire:get_status(#webdrv_opts{ url = ?SELENIUM }), 43 | io:format("Status: ~p\n", [R1]), 44 | 45 | {ok, _Pid} = webdrv_session:start_session(test, ?SELENIUM, 46 | webdrv_cap:default_htmlunit(), 10000), 47 | R = webdrv_session:get_status(test), 48 | io:format("Status: ~p\n", [R]), 49 | 50 | webdrv_session:stop_session(test). 51 | 52 | 53 | test4() -> 54 | {ok, _Pid} = webdrv_session:start_session(test, ?CHROMEDRIVER, 55 | webdrv_cap:default_chrome(), 10000), 56 | webdrv_session:execute(test, <<"window.name = 'WINDOW-0';">>, []), 57 | open_window(test, "WINDOW-1"), 58 | %% webdrv_session:close_window(test), 59 | webdrv_session:set_window_focus(test, "WINDOW-0"), 60 | 61 | Res = webdrv_session:get_screenshot(test), 62 | io:format("Res: ~p\n", [Res]), 63 | 64 | webdrv_session:stop_session(test). 65 | 66 | 67 | test3() -> 68 | {ok, _Pid} = webdrv_session:start_session(test, ?SELENIUM, webdrv_cap:default_htmlunit(), 10000), 69 | webdrv_session:set_url(test, "http://localhost:8088/elements.html"), 70 | Code = webdrv_session:get_page_source(test), 71 | io:format("CODE: ~p\n", [Code]), 72 | 73 | Res = webdrv_session:find_element(test, "class name", "class1"), 74 | io:format("RES: ~p\n", [Res]), 75 | 76 | webdrv_session:stop_session(test). 77 | 78 | 79 | test2() -> 80 | {ok, _Pid} = webdrv_session:start_session(test, ?CHROMEDRIVER, webdrv_cap:default_chrome(), 10000), 81 | %% {ok, _Pid} = webdrv_session:start_session(test, ?SELENIUM, webdrv_cap:default_chrome(), 10000), 82 | 83 | R = webdrv_session:get_cache_status(test), 84 | io:format("~p\n", [R]), 85 | 86 | %% [ io:format("~p\n", [{element(1, webdrv_session:get_log(test, L)), L}]) 87 | %% [ io:format("~p\n", [webdrv_session:get_log(test, L)]) 88 | %% || L <- ["browser","driver","client","server"] ], 89 | 90 | webdrv_session:stop_session(test). 91 | 92 | test() -> 93 | {ok, _Pid} = webdrv_session:start_session(test, ?SELENIUM, webdrv_cap:default_firefox(), 10000), 94 | 95 | Res = webdrv_session:execute(test, <<"window.name = 'WINDOW0';">>, []), 96 | io:format("Res: ~p\n", [Res]), 97 | 98 | Res2 = webdrv_session:set_window_maximize(test, "DFSJFLKSDJKLFJLKJF"), 99 | io:format("Res: ~p\n", [Res2]), 100 | 101 | webdrv_session:set_url(test, "http://80.252.210.134:8001/webtest/windows.html"), 102 | 103 | {ok, E0} = webdrv_session:find_element(test, "name", "WINDOW0"), 104 | webdrv_session:click_element(test, E0), 105 | 106 | {ok, E1} = webdrv_session:find_element(test, "name", "WINDOW1"), 107 | webdrv_session:click_element(test, E1), 108 | 109 | {ok, E2} = webdrv_session:find_element(test, "name", "WINDOW2"), 110 | webdrv_session:click_element(test, E2), 111 | 112 | webdrv_session:set_window_focus(test, "WINDOW0"), 113 | webdrv_session:set_url(test, "http://80.252.210.134:8001/webtest/page0.html"), 114 | 115 | webdrv_session:set_window_focus(test, "WINDOW1"), 116 | webdrv_session:set_url(test, "http://80.252.210.134:8001/webtest/page2.html"), 117 | 118 | webdrv_session:set_window_focus(test, "WINDOW2"), 119 | webdrv_session:set_url(test, "http://80.252.210.134:8001/webtest/page1.html"), 120 | 121 | webdrv_session:set_window_focus(test, "WINDOW2"), 122 | io:format("~p\n", [webdrv_session:get_page_source(test)]), 123 | 124 | webdrv_session:set_window_focus(test, "WINDOW1"), 125 | io:format("~p\n", [webdrv_session:get_page_source(test)]), 126 | 127 | webdrv_session:set_window_focus(test, "WINDOW0"), 128 | io:format("~p\n", [webdrv_session:get_page_source(test)]), 129 | 130 | 131 | webdrv_session:stop_session(test). 132 | 133 | 134 | links_w_text(SessName) -> 135 | {ok, Elements} = webdrv_session:find_elements(test, "tag name", "a"), 136 | %% [ io:format("~p\n", [E2]) || E <- Elements, E2 <- webdrv_session:get_element_info(SessName, E) ], 137 | Hrefs = [ Href || E <- Elements, {ok, Href} <- [webdrv_session:element_attribute(SessName, E, "href")] ], 138 | Texts = [ Txt || E <- Elements, {ok, Txt} <- [webdrv_session:get_text(SessName, E)] ], 139 | io:format("Elements: ~p\n", [lists:zip3(Elements, Hrefs, Texts)]). 140 | 141 | all_elements() -> 142 | [ 143 | {{"id", "id1"}, 3}, 144 | %% {{"id", "id2"}, 5}, 145 | 146 | {{"name", "name1"}, 3}, 147 | %% {{"name", "name2"}, 5}, 148 | 149 | {{"css selector", "ul"}, 2}, 150 | 151 | {{"class name", "class2"}, 5}, 152 | 153 | {{"tag name", "ul"}, 2}, 154 | 155 | {{"link text", "Link1"}, 18}, 156 | 157 | {{"partial link text", "nk1"}, 18}, 158 | 159 | {{"xpath", "//html"}, 1}, 160 | {{"xpath", "/html/body/ul"}, 2} 161 | ]. 162 | 163 | all_elements2() -> 164 | [ 165 | {{"id", "id1"}, 3}, 166 | %% {{"id", "id2"}, 5}, 167 | 168 | {{"name", "name1"}, 3}, 169 | %% {{"name", "name2"}, 5}, 170 | 171 | {{"css selector", "ul"}, 2}, 172 | 173 | {{"class name", "class2"}, 5}, 174 | 175 | {{"tag name", "ul"}, 2}, 176 | 177 | {{"link text", "Link1"}, 18}, 178 | 179 | {{"partial link text", "nk1"}, 18}, 180 | 181 | {{"xpath", "//html"}, 1}, 182 | {{"xpath", "/html/body/ul"}, 2}, 183 | {{"xpath", "li"}, 20} 184 | ]. 185 | 186 | 187 | res_sel_html() -> 188 | [{{"id","id1"},{"id","id1"},fail}, 189 | {{"id","id1"},{"name","name1"},fail}, 190 | {{"id","id1"},{"css selector","ul"},fail}, 191 | {{"id","id1"},{"class name","class2"},fail}, 192 | {{"id","id1"},{"tag name","ul"},fail}, 193 | {{"id","id1"},{"link text","Link2"},fail}, 194 | {{"id","id1"},{"partial link text","nk2"},fail}, 195 | {{"id","id1"},{"xpath","html"},fail}, 196 | {{"id","id1"},{"xpath","/html/body/ul"},ok}, 197 | {{"id","id1"},{"xpath","/html/body/a[last()]"},ok}, 198 | {{"name","name1"},{"id","id1"},fail}, 199 | {{"name","name1"},{"name","name1"},fail}, 200 | {{"name","name1"},{"css selector","ul"},fail}, 201 | {{"name","name1"},{"class name","class2"},fail}, 202 | {{"name","name1"},{"tag name","ul"},fail}, 203 | {{"name","name1"},{"link text","Link2"},fail}, 204 | {{"name","name1"},{"partial link text","nk2"},fail}, 205 | {{"name","name1"},{"xpath","html"},fail}, 206 | {{"name","name1"},{"xpath","/html/body/ul"},ok}, 207 | {{"name","name1"},{"xpath","/html/body/a[last()]"},ok}, 208 | {{"css selector","ul"},{"id","id1"},ok}, 209 | {{"css selector","ul"},{"name","name1"},ok}, 210 | {{"css selector","ul"},{"css selector","ul"},fail}, 211 | {{"css selector","ul"},{"class name","class2"},ok}, 212 | {{"css selector","ul"},{"tag name","ul"},fail}, 213 | {{"css selector","ul"},{"link text","Link2"},fail}, 214 | {{"css selector","ul"},{"partial link text","nk2"},fail}, 215 | {{"css selector","ul"},{"xpath","html"},fail}, 216 | {{"css selector","ul"},{"xpath","/html/body/ul"},ok}, 217 | {{"css selector","ul"},{"xpath","/html/body/a[last()]"},ok}, 218 | {{"class name","class2"},{"id","id1"},fail}, 219 | {{"class name","class2"},{"name","name1"},fail}, 220 | {{"class name","class2"},{"css selector","ul"},fail}, 221 | {{"class name","class2"},{"class name","class2"},fail}, 222 | {{"class name","class2"},{"tag name","ul"},fail}, 223 | {{"class name","class2"},{"link text","Link2"},fail}, 224 | {{"class name","class2"},{"partial link text","nk2"},fail}, 225 | {{"class name","class2"},{"xpath","html"},fail}, 226 | {{"class name","class2"},{"xpath","/html/body/ul"},ok}, 227 | {{"class name","class2"},{"xpath","/html/body/a[last()]"},ok}, 228 | {{"tag name","ul"},{"id","id1"},ok}, 229 | {{"tag name","ul"},{"name","name1"},ok}, 230 | {{"tag name","ul"},{"css selector","ul"},fail}, 231 | {{"tag name","ul"},{"class name","class2"},ok}, 232 | {{"tag name","ul"},{"tag name","ul"},fail}, 233 | {{"tag name","ul"},{"link text","Link2"},fail}, 234 | {{"tag name","ul"},{"partial link text","nk2"},fail}, 235 | {{"tag name","ul"},{"xpath","html"},fail}, 236 | {{"tag name","ul"},{"xpath","/html/body/ul"},ok}, 237 | {{"tag name","ul"},{"xpath","/html/body/a[last()]"},ok}, 238 | {{"link text","Link2"},{"id","id1"},fail}, 239 | {{"link text","Link2"},{"name","name1"},fail}, 240 | {{"link text","Link2"},{"css selector","ul"},fail}, 241 | {{"link text","Link2"},{"class name","class2"},fail}, 242 | {{"link text","Link2"},{"tag name","ul"},fail}, 243 | {{"link text","Link2"},{"link text","Link2"},fail}, 244 | {{"link text","Link2"},{"partial link text","nk2"},fail}, 245 | {{"link text","Link2"},{"xpath","html"},fail}, 246 | {{"link text","Link2"},{"xpath","/html/body/ul"},ok}, 247 | {{"link text","Link2"},{"xpath","/html/body/a[last()]"},ok}, 248 | {{"partial link text","nk2"},{"id","id1"},fail}, 249 | {{"partial link text","nk2"},{"name","name1"},fail}, 250 | {{"partial link text","nk2"},{"css selector","ul"},fail}, 251 | {{"partial link text","nk2"},{"class name","class2"},fail}, 252 | {{"partial link text","nk2"},{"tag name","ul"},fail}, 253 | {{"partial link text","nk2"},{"link text","Link2"},fail}, 254 | {{"partial link text","nk2"},{"partial link text","nk2"},fail}, 255 | {{"partial link text","nk2"},{"xpath","html"},fail}, 256 | {{"partial link text","nk2"},{"xpath","/html/body/ul"},ok}, 257 | {{"partial link text","nk2"},{"xpath","/html/body/a[last()]"},ok}, 258 | {{"xpath","html"},{"id","id1"},ok}, 259 | {{"xpath","html"},{"name","name1"},ok}, 260 | {{"xpath","html"},{"css selector","ul"},ok}, 261 | {{"xpath","html"},{"class name","class2"},ok}, 262 | {{"xpath","html"},{"tag name","ul"},ok}, 263 | {{"xpath","html"},{"link text","Link2"},ok}, 264 | {{"xpath","html"},{"partial link text","nk2"},ok}, 265 | {{"xpath","html"},{"xpath","html"},fail}, 266 | {{"xpath","html"},{"xpath","/html/body/ul"},ok}, 267 | {{"xpath","html"},{"xpath","/html/body/a[last()]"},ok}, 268 | {{"xpath","/html/body/ul"},{"id","id1"},ok}, 269 | {{"xpath","/html/body/ul"},{"name","name1"},ok}, 270 | {{"xpath","/html/body/ul"},{"css selector","ul"},fail}, 271 | {{"xpath","/html/body/ul"},{"class name","class2"},ok}, 272 | {{"xpath","/html/body/ul"},{"tag name","ul"},fail}, 273 | {{"xpath","/html/body/ul"},{"link text","Link2"},fail}, 274 | {{"xpath","/html/body/ul"},{"partial link text","nk2"},fail}, 275 | {{"xpath","/html/body/ul"},{"xpath","html"},fail}, 276 | {{"xpath","/html/body/ul"},{"xpath","/html/body/ul"},ok}, 277 | {{"xpath","/html/body/ul"},{"xpath","/html/body/a[last()]"},ok}, 278 | {{"xpath","/html/body/a[last()]"},{"id","id1"},fail}, 279 | {{"xpath","/html/body/a[last()]"},{"name","name1"},fail}, 280 | {{"xpath","/html/body/a[last()]"},{"css selector","ul"},fail}, 281 | {{"xpath","/html/body/a[last()]"},{"class name","class2"},fail}, 282 | {{"xpath","/html/body/a[last()]"},{"tag name","ul"},fail}, 283 | {{"xpath","/html/body/a[last()]"},{"link text","Link2"},fail}, 284 | {{"xpath","/html/body/a[last()]"},{"partial link text","nk2"},fail}, 285 | {{"xpath","/html/body/a[last()]"},{"xpath","html"},fail}, 286 | {{"xpath","/html/body/a[last()]"},{"xpath","/html/body/ul"},ok}, 287 | {{"xpath","/html/body/a[last()]"},{"xpath","/html/body/a[last()]"},ok}] 288 | . 289 | -------------------------------------------------------------------------------- /src/json.erl: -------------------------------------------------------------------------------- 1 | %% JSON - RFC 4627 - for Erlang 2 | %%--------------------------------------------------------------------------- 3 | %% @author Tony Garnock-Jones 4 | %% @author LShift Ltd. 5 | %% @copyright 2007-2010, 2011, 2012 Tony Garnock-Jones and 2007-2010 LShift Ltd. 6 | %% 7 | %% @reference RFC 8 | %% 4627, the JSON RFC 9 | %% 10 | %% @reference JSON in general 11 | %% 12 | %% @reference Joe Armstrong's 14 | %% message describing the basis of the JSON data type mapping that 15 | %% this module uses 16 | %% 17 | %% @doc An implementation of RFC 4627 (JSON, the JavaScript Object Notation) for Erlang. 18 | %% 19 | %% The basic API is comprised of the {@link encode/1} and {@link decode/1} functions. 20 | %% 21 | %% == Data Type Mapping == 22 | %% 23 | %% The data type mapping I've implemented is as per Joe Armstrong's 24 | %% message [http://www.erlang.org/ml-archive/erlang-questions/200511/msg00193.html] - see {@link json()}. 25 | %% 26 | %% == Unicode == 27 | %% 28 | %% When serializing a string, if characters are found with codepoint 29 | %% >127, we rely on the unicode encoder to build the proper byte 30 | %% sequence for transmission. We still use the \uXXXX escape for 31 | %% control characters (other than the RFC-specified specially 32 | %% recognised ones). 33 | %% 34 | %% {@link decode/1} will autodetect the unicode encoding used, and any 35 | %% strings returned in the result as binaries will contain UTF-8 36 | %% encoded byte sequences for codepoints >127. Object keys containing 37 | %% codepoints >127 will be returned as lists of codepoints, rather 38 | %% than being UTF-8 encoded. If you have already transformed the text 39 | %% to parse into a list of unicode codepoints, perhaps by your own use 40 | %% of {@link unicode_decode/1}, then use {@link decode_noauto/1} to 41 | %% avoid redundant and erroneous double-unicode-decoding. 42 | %% 43 | %% Similarly, {@link encode/1} produces text that is already UTF-8 44 | %% encoded. To get raw codepoints, use {@link encode_noauto/1} and 45 | %% {@link encode_noauto/2}. You can use {@link unicode_encode/1} to 46 | %% UTF-encode the results, if that's appropriate for your application. 47 | %% 48 | %% == Differences to the specification == 49 | %% 50 | %% I'm lenient in the following ways during parsing: 51 | %% 52 | %%
    53 | %%
  • repeated commas in arrays and objects collapse to a single comma
  • 54 | %%
  • any character =<32 is considered whitespace
  • 55 | %%
  • leading zeros for numbers are accepted
  • 56 | %%
  • we don't restrict the toplevel token to only object or array - 57 | %% any JSON value can be used at toplevel
  • 58 | %%
59 | %%

License

60 | %% 61 | %%
  Permission is hereby granted, free of charge, to any person
 62 | %% obtaining a copy of this software and associated documentation
 63 | %% files (the "Software"), to deal in the Software without
 64 | %% restriction, including without limitation the rights to use, copy,
 65 | %% modify, merge, publish, distribute, sublicense, and/or sell copies
 66 | %% of the Software, and to permit persons to whom the Software is
 67 | %% furnished to do so, subject to the following conditions:
 68 | %%
 69 | %% The above copyright notice and this permission notice shall be
 70 | %% included in all copies or substantial portions of the Software.
 71 | %%
 72 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 73 | %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 74 | %% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 75 | %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 76 | %% BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 77 | %% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 78 | %% CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 79 | %% SOFTWARE. 
80 | 81 | %% @type json() = jsonobj() | jsonarray() | jsonnum() | jsonstr() | true | false | null. An Erlang representation of a general JSON value. 82 | %% @type jsonobj() = {obj, [{jsonkey(), json()}]}. A JSON "object" or "struct". 83 | %% @type jsonkey() = string(). A field-name within a JSON "object". 84 | %% @type jsonarray() = [json()]. A JSON array value. 85 | %% @type jsonnum() = integer() | float(). A JSON numeric value. 86 | %% @type jsonstr() = binary(). A JSON string value. 87 | 88 | -module(json). 89 | 90 | -export([mime_type/0, encode/1, decode/1]). 91 | -export([encode_noauto/1, encode_noauto/2, decode_noauto/1]). 92 | -export([unicode_decode/1, unicode_encode/1]). 93 | -export([from_record/3, to_record/3]). 94 | -export([hex_digit/1, digit_hex/1]). 95 | -export([get_field/2, get_field/3, set_field/3, exclude_field/2]). 96 | -export([equiv/2]). 97 | 98 | %% @spec () -> string() 99 | %% @doc Returns the IANA-registered MIME type for JSON data. 100 | mime_type() -> 101 | "application/json; charset=utf-8". 102 | 103 | %% @spec (json()) -> [byte()] 104 | %% 105 | %% @doc Encodes the JSON value supplied, first into Unicode 106 | %% codepoints, and then into UTF-8. 107 | %% 108 | %% The resulting string is a list of byte values that should be 109 | %% interpreted as UTF-8 encoded text. 110 | %% 111 | %% During encoding, atoms and binaries are accepted as keys of JSON 112 | %% objects (type {@link jsonkey()}) as well as the usual strings 113 | %% (lists of character codepoints). 114 | encode(X) -> 115 | unicode_encode({'utf-8', encode_noauto(X)}). 116 | 117 | %% @spec (json()) -> string() 118 | %% 119 | %% @doc Encodes the JSON value supplied into raw Unicode codepoints. 120 | %% 121 | %% The resulting string may contain codepoints with value >=128. You 122 | %% can use {@link unicode_encode/1} to UTF-encode the results, if 123 | %% that's appropriate for your application. 124 | %% 125 | %% During encoding, atoms and binaries are accepted as keys of JSON 126 | %% objects (type {@link jsonkey()}) as well as the usual strings 127 | %% (lists of character codepoints). 128 | encode_noauto(X) -> 129 | lists:reverse(encode_noauto(X, [])). 130 | 131 | %% @spec (json(), string()) -> string() 132 | %% 133 | %% @doc As {@link encode_noauto/1}, but prepends reversed text 134 | %% to the supplied accumulator string. 135 | encode_noauto(true, Acc) -> 136 | "eurt" ++ Acc; 137 | encode_noauto(false, Acc) -> 138 | "eslaf" ++ Acc; 139 | encode_noauto(null, Acc) -> 140 | "llun" ++ Acc; 141 | encode_noauto(Str, Acc) when is_binary(Str) -> 142 | Codepoints = xmerl_ucs:from_utf8(Str), 143 | quote_and_encode_string(Codepoints, Acc); 144 | encode_noauto(Str, Acc) when is_atom(Str) -> 145 | quote_and_encode_string(atom_to_list(Str), Acc); 146 | encode_noauto(Num, Acc) when is_number(Num) -> 147 | encode_number(Num, Acc); 148 | encode_noauto({obj, Fields}, Acc) -> 149 | "}" ++ encode_object(Fields, "{" ++ Acc); 150 | encode_noauto(Dict, Acc) when element(1, Dict) =:= dict -> 151 | "}" ++ encode_object(dict:to_list(Dict), "{" ++ Acc); 152 | encode_noauto(Arr, Acc) when is_list(Arr) -> 153 | "]" ++ encode_array(Arr, "[" ++ Acc). 154 | 155 | encode_object([], Acc) -> 156 | Acc; 157 | encode_object([{Key, Value}], Acc) -> 158 | encode_field(Key, Value, Acc); 159 | encode_object([{Key, Value} | Rest], Acc) -> 160 | encode_object(Rest, "," ++ encode_field(Key, Value, Acc)). 161 | 162 | encode_field(Key, Value, Acc) when is_binary(Key) -> 163 | Codepoints = xmerl_ucs:from_utf8(Key), 164 | encode_noauto(Value, ":" ++ quote_and_encode_string(Codepoints, Acc)); 165 | encode_field(Key, Value, Acc) when is_atom(Key) -> 166 | encode_noauto(Value, ":" ++ quote_and_encode_string(atom_to_list(Key), Acc)); 167 | encode_field(Key, Value, Acc) when is_list(Key) -> 168 | encode_noauto(Value, ":" ++ quote_and_encode_string(Key, Acc)). 169 | 170 | encode_array([], Acc) -> 171 | Acc; 172 | encode_array([X], Acc) -> 173 | encode_noauto(X, Acc); 174 | encode_array([X | Rest], Acc) -> 175 | encode_array(Rest, "," ++ encode_noauto(X, Acc)). 176 | 177 | quote_and_encode_string(Str, Acc) -> 178 | "\"" ++ encode_string(Str, "\"" ++ Acc). 179 | 180 | encode_string([], Acc) -> 181 | Acc; 182 | encode_string([$" | Rest], Acc) -> 183 | encode_string(Rest, [$", $\\ | Acc]); 184 | encode_string([$\\ | Rest], Acc) -> 185 | encode_string(Rest, [$\\, $\\ | Acc]); 186 | encode_string([X | Rest], Acc) when X < 32 orelse X > 127 -> 187 | encode_string(Rest, encode_general_char(X, Acc)); 188 | encode_string([X | Rest], Acc) -> 189 | encode_string(Rest, [X | Acc]). 190 | 191 | encode_general_char(8, Acc) -> [$b, $\\ | Acc]; 192 | encode_general_char(9, Acc) -> [$t, $\\ | Acc]; 193 | encode_general_char(10, Acc) -> [$n, $\\ | Acc]; 194 | encode_general_char(12, Acc) -> [$f, $\\ | Acc]; 195 | encode_general_char(13, Acc) -> [$r, $\\ | Acc]; 196 | encode_general_char(X, Acc) when X > 127 -> [X | Acc]; 197 | encode_general_char(X, Acc) -> 198 | %% FIXME currently this branch never runs. 199 | %% We could make it configurable, maybe? 200 | Utf16Bytes = xmerl_ucs:to_utf16be(X), 201 | encode_utf16be_chars(Utf16Bytes, Acc). 202 | 203 | encode_utf16be_chars([], Acc) -> 204 | Acc; 205 | encode_utf16be_chars([B1, B2 | Rest], Acc) -> 206 | encode_utf16be_chars(Rest, [hex_digit((B2) band 16#F), 207 | hex_digit((B2 bsr 4) band 16#F), 208 | hex_digit((B1) band 16#F), 209 | hex_digit((B1 bsr 4) band 16#F), 210 | $u, 211 | $\\ | Acc]). 212 | 213 | %% @spec (Nibble::integer()) -> char() 214 | %% @doc Returns the character code corresponding to Nibble. 215 | %% 216 | %% Nibble must be >=0 and =<15. 217 | hex_digit(N) when is_integer(N), N >= 0, N =< 9 -> $0 + N; 218 | hex_digit(N) when is_integer(N), N >= 10, N =< 15 -> $A + N - 10. 219 | 220 | encode_number(Num, Acc) when is_integer(Num) -> 221 | lists:reverse(integer_to_list(Num), Acc); 222 | encode_number(Num, Acc) when is_float(Num) -> 223 | lists:reverse(float_to_list(Num), Acc). 224 | 225 | %% @spec (Input::(binary() | [byte()])) -> ({ok, json(), Remainder} | {error, Reason}) 226 | %% where Remainder = string() 227 | %% Reason = any() 228 | %% 229 | %% @doc Decodes a JSON value from an input binary or string of 230 | %% Unicode-encoded text. 231 | %% 232 | %% Given a binary, converts it to a list of bytes. Given a 233 | %% list/string, interprets it as a list of bytes. 234 | %% 235 | %% Uses {@link unicode_decode/1} on its input, which results in a list 236 | %% of codepoints, and then decodes a JSON value from that list of 237 | %% codepoints. 238 | %% 239 | %% Returns either `{ok, Result, Remainder}', where Remainder is the 240 | %% remaining portion of the input that was not consumed in the process 241 | %% of decoding Result, or `{error, Reason}'. 242 | decode(Bin) when is_binary(Bin) -> 243 | decode(binary_to_list(Bin)); 244 | decode(Bytes) -> 245 | {_Charset, Codepoints} = unicode_decode(Bytes), 246 | decode_noauto(Codepoints). 247 | 248 | %% @spec (Input::string()) -> ({ok, json(), string()} | {error, any()}) 249 | %% 250 | %% @doc As {@link decode/1}, but does not perform Unicode decoding on its input. 251 | %% 252 | %% Expects a list of codepoints - an ordinary Erlang string - rather 253 | %% than a list of Unicode-encoded bytes. 254 | decode_noauto(Bin) when is_binary(Bin) -> 255 | decode_noauto(binary_to_list(Bin)); 256 | decode_noauto(Chars) -> 257 | case catch parse(skipws(Chars)) of 258 | {'EXIT', Reason} -> 259 | %% Reason is usually far too much information, but helps 260 | %% if needing to debug this module. 261 | {error, Reason}; 262 | {Value, Remaining} -> 263 | {ok, Value, skipws(Remaining)} 264 | end. 265 | 266 | %% @spec ([byte()]) -> [char()] 267 | %% 268 | %% @doc Autodetects and decodes using the Unicode encoding of its input. 269 | %% 270 | %% From RFC4627, section 3, "Encoding": 271 | %% 272 | %%
273 | %% JSON text SHALL be encoded in Unicode. The default encoding is 274 | %% UTF-8. 275 | %% 276 | %% Since the first two characters of a JSON text will always be ASCII 277 | %% characters [RFC0020], it is possible to determine whether an octet 278 | %% stream is UTF-8, UTF-16 (BE or LE), or UTF-32 (BE or LE) by looking 279 | %% at the pattern of nulls in the first four octets. 280 | %% 281 | %% 00 00 00 xx UTF-32BE 282 | %% 00 xx 00 xx UTF-16BE 283 | %% xx 00 00 00 UTF-32LE 284 | %% xx 00 xx 00 UTF-16LE 285 | %% xx xx xx xx UTF-8 286 | %%
287 | %% 288 | %% Interestingly, the BOM (byte-order mark) is not mentioned. We 289 | %% support it here by using it to detect our encoding, discarding it 290 | %% if present, even though RFC4627 explicitly notes that the first two 291 | %% characters of a JSON text will be ASCII. 292 | %% 293 | %% If a BOM ([http://unicode.org/faq/utf_bom.html]) is present, we use 294 | %% that; if not, we use RFC4627's rules (as above). Note that UTF-32 295 | %% is the same as UCS-4 for our purposes (but see also 296 | %% [http://unicode.org/reports/tr19/tr19-9.html]). Note that UTF-16 is 297 | %% not the same as UCS-2! 298 | %% 299 | %% Note that I'm using xmerl's UCS/UTF support here. There's another 300 | %% UTF-8 codec in asn1rt, which works on binaries instead of lists. 301 | %% 302 | unicode_decode([0,0,254,255|C]) -> {'utf-32', xmerl_ucs:from_ucs4be(C)}; 303 | unicode_decode([255,254,0,0|C]) -> {'utf-32', xmerl_ucs:from_ucs4le(C)}; 304 | unicode_decode([254,255|C]) -> {'utf-16', xmerl_ucs:from_utf16be(C)}; 305 | unicode_decode([239,187,191|C]) -> {'utf-8', xmerl_ucs:from_utf8(C)}; 306 | unicode_decode(C=[0,0,_,_|_]) -> {'utf-32be', xmerl_ucs:from_ucs4be(C)}; 307 | unicode_decode(C=[_,_,0,0|_]) -> {'utf-32le', xmerl_ucs:from_ucs4le(C)}; 308 | unicode_decode(C=[0,_|_]) -> {'utf-16be', xmerl_ucs:from_utf16be(C)}; 309 | unicode_decode(C=[_,0|_]) -> {'utf-16le', xmerl_ucs:from_utf16le(C)}; 310 | unicode_decode(C=_) -> {'utf-8', xmerl_ucs:from_utf8(C)}. 311 | 312 | %% @spec (EncodingAndCharacters::{Encoding, [char()]}) -> [byte()] 313 | %% where Encoding = 'utf-32' | 'utf-32be' | 'utf-32le' | 'utf-16' | 314 | %% 'utf-16be' | 'utf-16le' | 'utf-8' 315 | %% 316 | %% @doc Encodes the given characters to bytes, using the given Unicode encoding. 317 | %% 318 | %% For convenience, we supply a partial inverse of unicode_decode; If 319 | %% a BOM is requested, we more-or-less arbitrarily pick the big-endian 320 | %% variant of the encoding, since big-endian is network-order. We 321 | %% don't support UTF-8 with BOM here. 322 | unicode_encode({'utf-32', C}) -> [0,0,254,255|xmerl_ucs:to_ucs4be(C)]; 323 | unicode_encode({'utf-32be', C}) -> xmerl_ucs:to_ucs4be(C); 324 | unicode_encode({'utf-32le', C}) -> xmerl_ucs:to_ucs4le(C); 325 | unicode_encode({'utf-16', C}) -> [254,255|xmerl_ucs:to_utf16be(C)]; 326 | unicode_encode({'utf-16be', C}) -> xmerl_ucs:to_utf16be(C); 327 | unicode_encode({'utf-16le', C}) -> xmerl_ucs:to_utf16le(C); 328 | unicode_encode({'utf-8', C}) -> xmerl_ucs:to_utf8(C). 329 | 330 | parse([$" | Rest]) -> %% " emacs balancing 331 | {Codepoints, Rest1} = parse_string(Rest, []), 332 | {list_to_binary(xmerl_ucs:to_utf8(Codepoints)), Rest1}; 333 | parse("true" ++ Rest) -> {true, Rest}; 334 | parse("false" ++ Rest) -> {false, Rest}; 335 | parse("null" ++ Rest) -> {null, Rest}; 336 | parse([${ | Rest]) -> parse_object(skipws(Rest), []); 337 | parse([$[ | Rest]) -> parse_array(skipws(Rest), []); 338 | parse([]) -> exit(unexpected_end_of_input); 339 | parse(Chars) -> parse_number(Chars, []). 340 | 341 | skipws([X | Rest]) when X =< 32 -> 342 | skipws(Rest); 343 | skipws(Chars) -> 344 | Chars. 345 | 346 | parse_string(Chars, Acc) -> 347 | case parse_codepoint(Chars) of 348 | {done, Rest} -> 349 | {lists:reverse(Acc), Rest}; 350 | {ok, Codepoint, Rest} -> 351 | parse_string(Rest, [Codepoint | Acc]) 352 | end. 353 | 354 | parse_codepoint([$" | Rest]) -> %% " emacs balancing 355 | {done, Rest}; 356 | parse_codepoint([$\\, Key | Rest]) -> 357 | parse_general_char(Key, Rest); 358 | parse_codepoint([X | Rest]) -> 359 | {ok, X, Rest}. 360 | 361 | parse_general_char($b, Rest) -> {ok, 8, Rest}; 362 | parse_general_char($t, Rest) -> {ok, 9, Rest}; 363 | parse_general_char($n, Rest) -> {ok, 10, Rest}; 364 | parse_general_char($f, Rest) -> {ok, 12, Rest}; 365 | parse_general_char($r, Rest) -> {ok, 13, Rest}; 366 | parse_general_char($/, Rest) -> {ok, $/, Rest}; 367 | parse_general_char($\\, Rest) -> {ok, $\\, Rest}; 368 | parse_general_char($", Rest) -> {ok, $", Rest}; 369 | parse_general_char($u, [D0, D1, D2, D3 | Rest]) -> 370 | Codepoint = 371 | (digit_hex(D0) bsl 12) + 372 | (digit_hex(D1) bsl 8) + 373 | (digit_hex(D2) bsl 4) + 374 | (digit_hex(D3)), 375 | if 376 | Codepoint >= 16#D800 andalso Codepoint < 16#DC00 -> 377 | % High half of surrogate pair 378 | case parse_codepoint(Rest) of 379 | {low_surrogate_pair, Codepoint2, Rest1} -> 380 | [FinalCodepoint] = 381 | xmerl_ucs:from_utf16be(<>), 383 | {ok, FinalCodepoint, Rest1}; 384 | _ -> 385 | exit(incorrect_usage_of_surrogate_pair) 386 | end; 387 | Codepoint >= 16#DC00 andalso Codepoint < 16#E000 -> 388 | {low_surrogate_pair, Codepoint, Rest}; 389 | true -> 390 | {ok, Codepoint, Rest} 391 | end. 392 | 393 | %% @spec (Hexchar::char()) -> integer() 394 | %% @doc Returns the number corresponding to Hexchar. 395 | %% 396 | %% Hexchar must be one of the characters `$0' through `$9', `$A' 397 | %% through `$F' or `$a' through `$f'. 398 | digit_hex(C) when is_integer(C), C >= $0, C =< $9 -> C - $0; 399 | digit_hex(C) when is_integer(C), C >= $A, C =< $F -> C - $A + 10; 400 | digit_hex(C) when is_integer(C), C >= $a, C =< $f -> C - $a + 10. 401 | 402 | finish_number(Acc, Rest) -> 403 | Str = lists:reverse(Acc), 404 | {case catch list_to_integer(Str) of 405 | {'EXIT', _} -> list_to_float(Str); 406 | Value -> Value 407 | end, Rest}. 408 | 409 | parse_number([$- | Rest], Acc) -> 410 | parse_number1(Rest, [$- | Acc]); 411 | parse_number(Rest = [C | _], Acc) -> 412 | case is_digit(C) of 413 | true -> parse_number1(Rest, Acc); 414 | false -> exit(syntax_error) 415 | end. 416 | 417 | parse_number1(Rest, Acc) -> 418 | {Acc1, Rest1} = parse_int_part(Rest, Acc), 419 | case Rest1 of 420 | [] -> finish_number(Acc1, []); 421 | [$. | More] -> 422 | {Acc2, Rest2} = parse_int_part(More, [$. | Acc1]), 423 | parse_exp(Rest2, Acc2, false); 424 | _ -> 425 | parse_exp(Rest1, Acc1, true) 426 | end. 427 | 428 | parse_int_part(Chars = [_Ch | _Rest], Acc) -> 429 | parse_int_part0(Chars, Acc). 430 | 431 | parse_int_part0([], Acc) -> 432 | {Acc, []}; 433 | parse_int_part0([Ch | Rest], Acc) -> 434 | case is_digit(Ch) of 435 | true -> parse_int_part0(Rest, [Ch | Acc]); 436 | false -> {Acc, [Ch | Rest]} 437 | end. 438 | 439 | parse_exp([$e | Rest], Acc, NeedFrac) -> 440 | parse_exp1(Rest, Acc, NeedFrac); 441 | parse_exp([$E | Rest], Acc, NeedFrac) -> 442 | parse_exp1(Rest, Acc, NeedFrac); 443 | parse_exp(Rest, Acc, _NeedFrac) -> 444 | finish_number(Acc, Rest). 445 | 446 | parse_exp1(Rest, Acc, NeedFrac) -> 447 | {Acc1, Rest1} = parse_signed_int_part(Rest, if 448 | NeedFrac -> [$e, $0, $. | Acc]; 449 | true -> [$e | Acc] 450 | end), 451 | finish_number(Acc1, Rest1). 452 | 453 | parse_signed_int_part([$+ | Rest], Acc) -> 454 | parse_int_part(Rest, [$+ | Acc]); 455 | parse_signed_int_part([$- | Rest], Acc) -> 456 | parse_int_part(Rest, [$- | Acc]); 457 | parse_signed_int_part(Rest, Acc) -> 458 | parse_int_part(Rest, Acc). 459 | 460 | is_digit(N) when is_integer(N) -> N >= $0 andalso N =< $9; 461 | is_digit(_) -> false. 462 | 463 | parse_object([$} | Rest], Acc) -> 464 | {{obj, lists:reverse(Acc)}, Rest}; 465 | parse_object([$, | Rest], Acc) -> 466 | parse_object(skipws(Rest), Acc); 467 | parse_object([$" | Rest], Acc) -> %% " emacs balancing 468 | {KeyCodepoints, Rest1} = parse_string(Rest, []), 469 | [$: | Rest2] = skipws(Rest1), 470 | {Value, Rest3} = parse(skipws(Rest2)), 471 | parse_object(skipws(Rest3), [{KeyCodepoints, Value} | Acc]). 472 | 473 | parse_array([$] | Rest], Acc) -> 474 | {lists:reverse(Acc), Rest}; 475 | parse_array([$, | Rest], Acc) -> 476 | parse_array(skipws(Rest), Acc); 477 | parse_array(Chars, Acc) -> 478 | {Value, Rest} = parse(Chars), 479 | parse_array(skipws(Rest), [Value | Acc]). 480 | 481 | %% @spec (Record, atom(), [any()]) -> jsonobj() 482 | %% where Record = tuple() 483 | %% 484 | %% @doc Used by the `?RFC4627_FROM_RECORD' macro in `rfc4627.hrl'. 485 | %% 486 | %% Given a record type definiton of ``-record(myrecord, {field1, 487 | %% field})'', and a value ``V = #myrecord{}'', the code 488 | %% ``?RFC4627_FROM_RECORD(myrecord, V)'' will return a JSON "object" 489 | %% with fields corresponding to the fields of the record. The macro 490 | %% expands to a call to the `from_record' function. 491 | from_record(R, _RecordName, Fields) -> 492 | {obj, encode_record_fields(R, 2, Fields)}. 493 | 494 | encode_record_fields(_R, _Index, []) -> 495 | []; 496 | encode_record_fields(R, Index, [Field | Rest]) -> 497 | case element(Index, R) of 498 | undefined -> 499 | encode_record_fields(R, Index + 1, Rest); 500 | Value -> 501 | [{atom_to_list(Field), Value} | encode_record_fields(R, Index + 1, Rest)] 502 | end. 503 | 504 | %% @spec (JsonObject::jsonobj(), DefaultValue::Record, [atom()]) -> Record 505 | %% where Record = tuple() 506 | %% 507 | %% @doc Used by the `?RFC4627_TO_RECORD' macro in `rfc4627.hrl'. 508 | %% 509 | %% Given a record type definiton of ``-record(myrecord, {field1, 510 | %% field})'', and a JSON "object" ``J = {obj, [{"field1", 123}, 511 | %% {"field2", 234}]}'', the code ``?RFC4627_TO_RECORD(myrecord, J)'' 512 | %% will return a record ``#myrecord{field1 = 123, field2 = 234}''. 513 | %% The macro expands to a call to the `to_record' function. 514 | to_record({obj, Values}, Fallback, Fields) -> 515 | list_to_tuple([element(1, Fallback) | decode_record_fields(Values, Fallback, 2, Fields)]). 516 | 517 | decode_record_fields(_Values, _Fallback, _Index, []) -> 518 | []; 519 | decode_record_fields(Values, Fallback, Index, [Field | Rest]) -> 520 | [case lists:keysearch(atom_to_list(Field), 1, Values) of 521 | {value, {_, Value}} -> 522 | Value; 523 | false -> 524 | element(Index, Fallback) 525 | end | decode_record_fields(Values, Fallback, Index + 1, Rest)]. 526 | 527 | %% @spec (JsonObject::jsonobj(), atom()) -> jsonobj() 528 | %% @doc Exclude a named field from a JSON "object". 529 | exclude_field({obj, Props}, Key) -> 530 | {obj, lists:keydelete(Key, 1, Props)}. 531 | 532 | %% @spec (JsonObject::jsonobj(), atom()) -> {ok, json()} | not_found 533 | %% @doc Retrieves the value of a named field of a JSON "object". 534 | get_field({obj, Props}, Key) -> 535 | case lists:keysearch(Key, 1, Props) of 536 | {value, {_K, Val}} -> 537 | {ok, Val}; 538 | false -> 539 | not_found 540 | end. 541 | 542 | %% @spec (jsonobj(), atom(), json()) -> json() 543 | %% @doc Retrieves the value of a named field of a JSON "object", or a 544 | %% default value if no such field is present. 545 | get_field(Obj, Key, DefaultValue) -> 546 | case get_field(Obj, Key) of 547 | {ok, Val} -> 548 | Val; 549 | not_found -> 550 | DefaultValue 551 | end. 552 | 553 | %% @spec (JsonObject::jsonobj(), atom(), json()) -> jsonobj() 554 | %% @doc Adds or replaces a named field with the given value. 555 | %% 556 | %% Returns a JSON "object" that contains the new field value as well 557 | %% as all the unmodified fields from the first argument. 558 | set_field({obj, Props}, Key, NewValue) -> 559 | {obj, [{Key, NewValue} | lists:keydelete(Key, 1, Props)]}. 560 | 561 | %% @spec (A::json(), B::json()) -> bool() 562 | %% @doc Tests equivalence of JSON terms. 563 | %% 564 | %% After Bob Ippolito's `equiv' predicate in mochijson. 565 | equiv({obj, Props1}, {obj, Props2}) -> 566 | L1 = lists:keysort(1, Props1), 567 | L2 = lists:keysort(1, Props2), 568 | equiv_sorted_plists(L1, L2); 569 | equiv(A, B) when is_list(A) andalso is_list(B) -> 570 | equiv_arrays(A, B); 571 | equiv(A, B) -> 572 | A == B. 573 | 574 | equiv_sorted_plists([], []) -> true; 575 | equiv_sorted_plists([], _) -> false; 576 | equiv_sorted_plists(_, []) -> false; 577 | equiv_sorted_plists([{K1, V1} | R1], [{K2, V2} | R2]) -> 578 | K1 == K2 andalso equiv(V1, V2) andalso equiv_sorted_plists(R1, R2). 579 | 580 | equiv_arrays([], []) -> true; 581 | equiv_arrays([], _) -> false; 582 | equiv_arrays(_, []) -> false; 583 | equiv_arrays([V1 | R1], [V2 | R2]) -> 584 | equiv(V1, V2) andalso equiv_arrays(R1, R2). 585 | -------------------------------------------------------------------------------- /src/webdrv_wire.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Hans Svensson 3 | %%% @copyright (C) 2013, Quviq AB. 4 | %%% 5 | %%% @doc WebDriver Wire Protocol 6 | %%% 7 | %%% The individual functions are not documented. They have type annotations, for more 8 | %%% information on the particular functions, we refer to (The WebDriver 10 | %%% Wire Protocol). 11 | %%% 12 | %%% The errors reported are slightly more advanced that the basic error reporting 13 | %%% prescribed in the protocol description; see the type {@link request_error()}. 14 | %%% 15 | %%%

License

16 | %%%
 17 | %%% Permission is hereby granted, free of charge, to any person
 18 | %%% obtaining a copy of this software and associated documentation
 19 | %%% files (the "Software"), to deal in the Software without
 20 | %%% restriction, including without limitation the rights to use, copy,
 21 | %%% modify, merge, publish, distribute, sublicense, and/or sell copies
 22 | %%% of the Software, and to permit persons to whom the Software is
 23 | %%% furnished to do so, subject to the following conditions:
 24 | %%%
 25 | %%% The above copyright notice and this permission notice shall be
 26 | %%% included in all copies or substantial portions of the Software.
 27 | %%%
 28 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 29 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 30 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 31 | %%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 32 | %%% BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 33 | %%% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 34 | %%% CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 35 | %%% SOFTWARE. 
36 | %%% @end 37 | %%%------------------------------------------------------------------- 38 | 39 | -module(webdrv_wire). 40 | 41 | -export([get_status/1, start_session/2, start_session/3, 42 | get_sessions/1, session/1, stop_session/1, 43 | set_timeout/3, set_async_script_timeout/2, set_implicit_wait_timeout/2, 44 | get_window_handle/1, get_window_handles/1, get_url/1, 45 | set_url/2, forward/1, back/1, 46 | refresh/1, execute/3, execute_async/3, 47 | get_screenshot/1, get_ime_available_engines/1, get_ime_active_engine/1, 48 | get_ime_activated/1, ime_deactivate/1, ime_activate/2, 49 | set_frame/2, set_window_focus/2, close_window/1, 50 | set_window_size/3, set_window_size/4, get_window_size/1, 51 | get_window_size/2, set_window_position/3, set_window_position/4, 52 | get_window_position/1, get_window_position/2, set_window_maximize/1, 53 | set_window_maximize/2, get_cookies/1, add_cookie/2, 54 | delete_cookies/1, delete_cookie/2, get_page_source/1, 55 | get_page_title/1, find_element/3, find_elements/3, 56 | get_active_element/1, get_element_info/2, find_element_rel/4, 57 | find_elements_rel/4, click_element/2, submit/2, 58 | get_text/2, send_value/3, send_keys/2, 59 | element_name/2, clear_element/2, is_selected_element/2, 60 | is_enabled_element/2, element_attribute/3, are_elements_equal/3, 61 | is_displayed_element/2, element_location/2, is_element_location_in_view/2, 62 | element_size/2, element_css_property/3, get_browser_orientation/1, 63 | set_browser_orientation/2, get_alert_text/1, set_alert_text/2, 64 | accept_alert/1, dismiss_alert/1, moveto/4, 65 | click/2, buttondown/2, buttonup/2, 66 | doubleclick/1, get_log/2, get_log_types/1, get_cache_status/1]). 67 | 68 | -include("../include/webdrv.hrl"). 69 | 70 | %% Helper functions for options 71 | session_id(#webdrv_opts{ session_id = SId }) -> SId. 72 | url(#webdrv_opts{ url = Url }) -> Url. 73 | timeout(#webdrv_opts{ timeout = Timeout }) -> Timeout. 74 | 75 | %% API 76 | get_status(Opts) -> 77 | do_get_cmd(Opts, "status"). 78 | 79 | -spec start_session(#webdrv_opts{}, capability()) -> request_res(). 80 | start_session(Opts, Capability) -> 81 | start_session(Opts, Capability, null). 82 | 83 | -spec start_session(#webdrv_opts{}, capability(), capability()) -> request_res(). 84 | start_session(Opts, Desired, Required) -> 85 | Params = [{desiredCapabilities, webdrv_cap:to_json(Desired)}, 86 | {requiredCapabilities, webdrv_cap:to_json(Required)}], 87 | do_post_cmd(Opts, "session", Params). 88 | 89 | -spec get_sessions(#webdrv_opts{}) -> request_res(). 90 | get_sessions(Opts) -> 91 | do_get_cmd(Opts, "sessions"). 92 | 93 | -spec session(#webdrv_opts{}) -> request_res(). 94 | session(Opts) -> 95 | do_get_cmd(Opts, "session/" ++ session_id(Opts)). 96 | 97 | -spec stop_session(#webdrv_opts{}) -> request_res(). 98 | stop_session(Opts) -> 99 | do_delete_cmd(Opts, "session/" ++ session_id(Opts)). 100 | 101 | -spec set_timeout(#webdrv_opts{}, term(), term()) -> request_res(). 102 | set_timeout(Opts, TimeoutType, Timeout) -> 103 | Params = [{type, TimeoutType}, {ms, Timeout}], 104 | do_post_scmd(Opts, "timeouts", Params). 105 | 106 | -spec set_async_script_timeout(#webdrv_opts{}, number()) -> request_res(). 107 | set_async_script_timeout(Opts, Timeout) -> 108 | do_post_scmd(Opts, "timeouts/async_script", [{ms, Timeout}]). 109 | 110 | -spec set_implicit_wait_timeout(#webdrv_opts{}, number()) -> request_res(). 111 | set_implicit_wait_timeout(Opts, Timeout) -> 112 | do_post_scmd(Opts, "timeouts/implicit_wait", [{ms, Timeout}]). 113 | 114 | -spec get_window_handle(#webdrv_opts{}) -> request_res(). 115 | get_window_handle(Opts) -> 116 | do_get_scmd(Opts, "window_handle"). 117 | 118 | -spec get_window_handles(#webdrv_opts{}) -> request_res(). 119 | get_window_handles(Opts) -> 120 | do_get_scmd(Opts, "window_handles"). 121 | 122 | -spec get_url(#webdrv_opts{}) -> request_res(). 123 | get_url(Opts) -> 124 | do_get_scmd(Opts, "url"). 125 | 126 | -spec set_url(#webdrv_opts{}, jsonstr()) -> request_res(). 127 | set_url(Opts, Url) -> 128 | do_post_scmd(Opts, "url", [{url, Url}]). 129 | 130 | -spec forward(#webdrv_opts{}) -> request_res(). 131 | forward(Opts) -> 132 | do_post_scmd(Opts, "forward", []). 133 | 134 | -spec back(#webdrv_opts{}) -> request_res(). 135 | back(Opts) -> 136 | do_post_scmd(Opts, "back", []). 137 | 138 | -spec refresh(#webdrv_opts{}) -> request_res(). 139 | refresh(Opts) -> 140 | do_post_scmd(Opts, "refresh", []). 141 | 142 | -spec execute(#webdrv_opts{}, jsonstr(), jsonlist()) -> request_res(). 143 | execute(Opts, Script, Args) -> 144 | do_post_scmd(Opts, "execute", [{script, Script}, {args, Args}]). 145 | 146 | -spec execute_async(#webdrv_opts{}, jsonstr(), jsonlist()) -> request_res(). 147 | execute_async(Opts, Script, Args) -> 148 | do_post_scmd(Opts, "execute_async", [{script, Script}, {args, Args}]). 149 | 150 | -spec get_screenshot(#webdrv_opts{}) -> request_res(). 151 | get_screenshot(Opts) -> 152 | do_get_scmd(Opts, "screenshot"). 153 | 154 | -spec get_ime_available_engines(#webdrv_opts{}) -> request_res(). 155 | get_ime_available_engines(Opts) -> 156 | do_get_scmd(Opts, "ime/available_engines"). 157 | 158 | -spec get_ime_active_engine(#webdrv_opts{}) -> request_res(). 159 | get_ime_active_engine(Opts) -> 160 | do_get_scmd(Opts, "ime/active_engine"). 161 | 162 | -spec get_ime_activated(#webdrv_opts{}) -> request_res(). 163 | get_ime_activated(Opts) -> 164 | do_get_scmd(Opts, "ime/activated"). 165 | 166 | -spec ime_deactivate(#webdrv_opts{}) -> request_res(). 167 | ime_deactivate(Opts) -> 168 | do_post_scmd(Opts, "ime/deactivate", []). 169 | 170 | -spec ime_activate(#webdrv_opts{}, jsonstr()) -> request_res(). 171 | ime_activate(Opts, Engine) -> 172 | do_post_scmd(Opts, "ime/activate", [{engine, Engine}]). 173 | 174 | -spec set_frame(#webdrv_opts{}, frame_id()) -> request_res(). 175 | set_frame(Opts, Frame) -> 176 | do_post_scmd(Opts, "frame", [{id, Frame}]). 177 | 178 | -spec set_window_focus(#webdrv_opts{}, jsonstr()) -> request_res(). 179 | set_window_focus(Opts, Window) -> 180 | do_post_scmd(Opts, "window", [{name, Window}]). 181 | 182 | -spec close_window(#webdrv_opts{}) -> request_res(). 183 | close_window(Opts) -> 184 | do_delete_scmd(Opts, "window"). 185 | 186 | -spec set_window_size(#webdrv_opts{}, number(), number()) -> request_res(). 187 | set_window_size(Opts, Width, Height) -> 188 | set_window_size(Opts, "current", Width, Height). 189 | 190 | -spec set_window_size(#webdrv_opts{}, jsonstr(), number(), number()) -> request_res(). 191 | set_window_size(Opts, WindowHandle, Width, Height) -> 192 | do_post_scmd(Opts, "window/" ++ WindowHandle ++ "/size", 193 | [{width, Width}, {height, Height}]). 194 | 195 | -spec get_window_size(#webdrv_opts{}) -> request_res(). 196 | get_window_size(Opts) -> 197 | get_window_size(Opts, "current"). 198 | 199 | -spec get_window_size(#webdrv_opts{}, jsonstr()) -> request_res(). 200 | get_window_size(Opts, WindowHandle) -> 201 | do_get_scmd(Opts, "window/" ++ WindowHandle ++ "/size"). 202 | 203 | -spec set_window_position(#webdrv_opts{}, number(), number()) -> request_res(). 204 | set_window_position(Opts, X, Y) -> 205 | set_window_position(Opts, "current", X, Y). 206 | 207 | -spec set_window_position(#webdrv_opts{}, jsonstr(), number(), number()) -> request_res(). 208 | set_window_position(Opts, WindowHandle, X, Y) -> 209 | do_post_scmd(Opts, "window/" ++ WindowHandle ++ "/position", [{x, X}, {y, Y}]). 210 | 211 | -spec get_window_position(#webdrv_opts{}) -> request_res(). 212 | get_window_position(Opts) -> 213 | get_window_position(Opts, "current"). 214 | 215 | -spec get_window_position(#webdrv_opts{}, jsonstr()) -> request_res(). 216 | get_window_position(Opts, WindowHandle) -> 217 | do_get_scmd(Opts, "window/" ++ WindowHandle ++ "/position"). 218 | 219 | -spec set_window_maximize(#webdrv_opts{}) -> request_res(). 220 | set_window_maximize(Opts) -> 221 | set_window_maximize(Opts, "current"). 222 | 223 | -spec set_window_maximize(#webdrv_opts{}, jsonstr()) -> request_res(). 224 | set_window_maximize(Opts, WindowHandle) -> 225 | do_post_scmd(Opts, "window/" ++ WindowHandle ++ "/maximize", []). 226 | 227 | -spec get_cookies(#webdrv_opts{}) -> request_res(). 228 | get_cookies(Opts) -> 229 | do_get_scmd(Opts, "cookie"). 230 | 231 | -spec add_cookie(#webdrv_opts{}, jsonobj()) -> request_res(). 232 | add_cookie(Opts, Cookie) -> 233 | do_post_scmd(Opts, "cookie", [{cookie, Cookie}]). 234 | 235 | -spec delete_cookies(#webdrv_opts{}) -> request_res(). 236 | delete_cookies(Opts) -> 237 | do_delete_scmd(Opts, "cookie"). 238 | 239 | -spec delete_cookie(#webdrv_opts{}, jsonstr()) -> request_res(). 240 | delete_cookie(Opts, Name) -> 241 | do_delete_scmd(Opts, "cookie/" ++ Name). 242 | 243 | -spec get_page_source(#webdrv_opts{}) -> request_res(). 244 | get_page_source(Opts) -> 245 | do_get_scmd(Opts, "source"). 246 | 247 | -spec get_page_title(#webdrv_opts{}) -> request_res(). 248 | get_page_title(Opts) -> 249 | do_get_scmd(Opts, "title"). 250 | 251 | -spec find_element(#webdrv_opts{}, jsonstr(), jsonstr()) -> request_res(). 252 | find_element(Opts, Strategy, Value) -> 253 | do_post_scmd(Opts, "element", [{using, Strategy}, {value, Value}]). 254 | 255 | -spec find_elements(#webdrv_opts{}, jsonstr(), jsonstr()) -> request_res(). 256 | find_elements(Opts, Strategy, Value) -> 257 | do_post_scmd(Opts, "elements", [{using, Strategy}, {value, Value}]). 258 | 259 | -spec get_active_element(#webdrv_opts{}) -> request_res(). 260 | get_active_element(Opts) -> 261 | do_post_scmd(Opts, "element/active", []). 262 | 263 | %% Currently undefined 264 | -spec get_element_info(#webdrv_opts{}, jsonstr()) -> request_res(). 265 | get_element_info(Opts, ElementId) -> 266 | do_get_scmd(Opts, "element/" ++ ElementId). 267 | 268 | -spec find_element_rel(#webdrv_opts{}, jsonstr(), jsonstr(), jsonstr()) -> request_res(). 269 | find_element_rel(Opts, ElementId, Strategy, Value) -> 270 | do_post_ecmd(Opts, ElementId, "element", [{using, Strategy}, {value, Value}]). 271 | 272 | -spec find_elements_rel(#webdrv_opts{}, jsonstr(), jsonstr(), jsonstr()) -> request_res(). 273 | find_elements_rel(Opts, ElementId, Strategy, Value) -> 274 | do_post_ecmd(Opts, ElementId, "elements", [{using, Strategy}, {value, Value}]). 275 | 276 | -spec click_element(#webdrv_opts{}, jsonstr()) -> request_res(). 277 | click_element(Opts, ElementId) -> 278 | do_post_ecmd(Opts, ElementId, "click", []). 279 | 280 | -spec submit(#webdrv_opts{}, jsonstr()) -> request_res(). 281 | submit(Opts, ElementId) -> 282 | do_post_ecmd(Opts, ElementId, "submit", []). 283 | 284 | -spec get_text(#webdrv_opts{}, jsonstr()) -> request_res(). 285 | get_text(Opts, ElementId) -> 286 | do_get_ecmd(Opts, ElementId, "text"). 287 | 288 | -spec send_value(#webdrv_opts{}, jsonstr(), jsonstr()) -> request_res(). 289 | send_value(Opts, ElementId, Value) -> 290 | do_post_ecmd(Opts, ElementId, "value", [{value, [Value]}]). 291 | 292 | -spec send_keys(#webdrv_opts{}, jsonstr()) -> request_res(). 293 | send_keys(Opts, Value) -> 294 | do_post_scmd(Opts, "keys", [{value, Value}]). 295 | 296 | -spec element_name(#webdrv_opts{}, jsonstr()) -> request_res(). 297 | element_name(Opts, ElementId) -> 298 | do_get_ecmd(Opts, ElementId, "name"). 299 | 300 | -spec clear_element(#webdrv_opts{}, jsonstr()) -> request_res(). 301 | clear_element(Opts, ElementId) -> 302 | do_post_ecmd(Opts, ElementId, "clear", []). 303 | 304 | -spec is_selected_element(#webdrv_opts{}, jsonstr()) -> request_res(). 305 | is_selected_element(Opts, ElementId) -> 306 | do_get_ecmd(Opts, ElementId, "selected"). 307 | 308 | -spec is_enabled_element(#webdrv_opts{}, jsonstr()) -> request_res(). 309 | is_enabled_element(Opts, ElementId) -> 310 | do_get_ecmd(Opts, ElementId, "enabled"). 311 | 312 | -spec element_attribute(#webdrv_opts{}, jsonstr(), jsonstr()) -> request_res(). 313 | element_attribute(Opts, ElementId, Name) -> 314 | do_get_ecmd(Opts, ElementId, "attribute/" ++ Name). 315 | 316 | -spec are_elements_equal(#webdrv_opts{}, jsonstr(), jsonstr()) -> request_res(). 317 | are_elements_equal(Opts, ElementId1, ElementId2) -> 318 | do_get_ecmd(Opts, ElementId1, "equals/" ++ ElementId2). 319 | 320 | -spec is_displayed_element(#webdrv_opts{}, jsonstr()) -> request_res(). 321 | is_displayed_element(Opts, ElementId) -> 322 | do_get_ecmd(Opts, ElementId, "displayed"). 323 | 324 | -spec element_location(#webdrv_opts{}, jsonstr()) -> request_res(). 325 | element_location(Opts, ElementId) -> 326 | do_get_ecmd(Opts, ElementId, "location"). 327 | 328 | -spec is_element_location_in_view(#webdrv_opts{}, jsonstr()) -> request_res(). 329 | is_element_location_in_view(Opts, ElementId) -> 330 | do_get_ecmd(Opts, ElementId, "location_in_view"). 331 | 332 | -spec element_size(#webdrv_opts{}, jsonstr()) -> request_res(). 333 | element_size(Opts, ElementId) -> 334 | do_get_ecmd(Opts, ElementId, "size"). 335 | 336 | -spec element_css_property(#webdrv_opts{}, jsonstr(), jsonstr()) -> request_res(). 337 | element_css_property(Opts, ElementId, Prop) -> 338 | do_get_ecmd(Opts, ElementId, "css/" ++ Prop). 339 | 340 | -spec get_browser_orientation(#webdrv_opts{}) -> request_res(). 341 | get_browser_orientation(Opts) -> 342 | do_get_scmd(Opts, "orientation"). 343 | 344 | -spec set_browser_orientation(#webdrv_opts{}, jsonstr()) -> request_res(). 345 | set_browser_orientation(Opts, Dir) -> 346 | do_post_scmd(Opts, "orientation", [{orientation, Dir}]). 347 | 348 | -spec get_alert_text(#webdrv_opts{}) -> request_res(). 349 | get_alert_text(Opts) -> 350 | do_get_scmd(Opts, "alert_text"). 351 | 352 | -spec set_alert_text(#webdrv_opts{}, jsonstr()) -> request_res(). 353 | set_alert_text(Opts, Str) -> 354 | do_post_scmd(Opts, "alert_text", [{text, Str}]). 355 | 356 | -spec accept_alert(#webdrv_opts{}) -> request_res(). 357 | accept_alert(Opts) -> 358 | do_post_scmd(Opts, "accept_alert", []). 359 | 360 | -spec dismiss_alert(#webdrv_opts{}) -> request_res(). 361 | dismiss_alert(Opts) -> 362 | do_post_scmd(Opts, "dismiss_alert", []). 363 | 364 | -spec moveto(#webdrv_opts{}, jsonstr(), number(), number()) -> request_res(). 365 | moveto(Opts, Elem, XOffSet, YOffSet) -> 366 | do_post_scmd(Opts, "moveto", 367 | [{element, Elem}, {xoffset, XOffSet}, {yoffset, YOffSet}]). 368 | 369 | -spec click(#webdrv_opts{}, number()) -> request_res(). 370 | click(Opts, Button) -> 371 | do_post_scmd(Opts, "click", [{button, Button}]). 372 | 373 | -spec buttondown(#webdrv_opts{}, number()) -> request_res(). 374 | buttondown(Opts, Button) -> 375 | do_post_scmd(Opts, "buttondown", [{button, Button}]). 376 | 377 | -spec buttonup(#webdrv_opts{}, number()) -> request_res(). 378 | buttonup(Opts, Button) -> 379 | do_post_scmd(Opts, "buttonup", [{button, Button}]). 380 | 381 | -spec doubleclick(#webdrv_opts{}) -> request_res(). 382 | doubleclick(Opts) -> 383 | do_post_scmd(Opts, "doubleclick", []). 384 | 385 | %% SKIP Touch, Geo and Local storage for now... 386 | 387 | -spec get_log(#webdrv_opts{}, jsonstr()) -> request_res(). 388 | get_log(Opts, LogType) -> 389 | do_post_scmd(Opts, "log", [{type, LogType}]). 390 | 391 | -spec get_log_types(#webdrv_opts{}) -> request_res(). 392 | get_log_types(Opts) -> 393 | do_get_scmd(Opts, "log/types"). 394 | 395 | -spec get_cache_status(#webdrv_opts{}) -> request_res(). 396 | get_cache_status(Opts) -> 397 | do_get_scmd(Opts, "application_cache/status"). 398 | 399 | 400 | %% ------------- 401 | do_post_ecmd(Opts, ElementId, Cmd, Params) -> 402 | do_post_scmd(Opts, "element/" ++ ElementId ++ "/" ++ Cmd, Params). 403 | 404 | do_get_ecmd(Opts, ElementId, Cmd) -> 405 | do_get_scmd(Opts, "element/" ++ ElementId ++ "/" ++ Cmd). 406 | 407 | do_post_scmd(Opts, Cmd, Params) -> 408 | do_post_cmd(Opts, "session/" ++ session_id(Opts) ++ "/" ++ Cmd, Params). 409 | 410 | do_get_scmd(Opts, Cmd) -> 411 | do_get_cmd(Opts, "session/" ++ session_id(Opts) ++ "/" ++ Cmd). 412 | 413 | do_post_cmd(Opts, Cmd, Params) -> 414 | URL = url(Opts) ++ Cmd, 415 | do_post(Opts, URL, {obj, Params}). 416 | 417 | do_get_cmd(Opts, Cmd) -> 418 | do_get(Opts, url(Opts) ++ Cmd). 419 | 420 | do_delete_scmd(Opts, Cmd) -> 421 | do_delete_cmd(Opts, "session/" ++ session_id(Opts) ++ "/" ++ Cmd). 422 | 423 | do_delete_cmd(Opts, Cmd) -> 424 | do_delete(Opts, url(Opts) ++ Cmd). 425 | 426 | %% HTML / HTTP functions 427 | do_post(Opts, Url, JSONParams) -> 428 | {ok, {_, _, Host, Port, _, _}} = http_uri:parse(Url), 429 | JSON = json:encode(JSONParams), 430 | Len = length(JSON), 431 | request(Opts, post, 432 | {Url, 433 | [{"Content-Length", integer_to_list(Len)}, 434 | {"Content-Type", json:mime_type()}, 435 | {"host", Host ++ ":" ++ integer_to_list(Port)}, 436 | {"connection", "keep-alive"} 437 | ], 438 | json:mime_type(), 439 | JSON}). 440 | 441 | do_get(Opts, Url) -> 442 | request(Opts, get, {Url, [{"Accept", "application/json"}]}). 443 | 444 | do_delete(Opts, URL) -> 445 | request(Opts, delete, {URL, []}). 446 | 447 | -spec request(#webdrv_opts{}, httpc:method(), httpc:request()) -> 448 | {ok, session_id(), jsonobj()} | request_error(). 449 | request(Opts, Method, Request) -> 450 | %% io:format("REQ: ~p\n", [{Method, Request}]), 451 | Res = httpc_request_bug_fix(Opts, Method, Request), 452 | case parse_httpc_result(Res) of 453 | {error, {txt, Err}} -> 454 | {html_error, Err}; 455 | {error, {json, JErr}} -> 456 | {json_error, JErr}; 457 | {cmd_fail, {ok, JSON}} -> 458 | {cmd_error, JSON}; 459 | {cmd_fail, {error, {json, JErr}}} -> 460 | {json_error, JErr}; %% This one could be discussed 461 | ok -> {ok, null, {obj, []}}; 462 | {ok, JsonTerm} -> check_json_response(JsonTerm) 463 | end. 464 | 465 | check_json_response(JsonTerm) -> 466 | case parse_response_json(JsonTerm) of 467 | {0, SessId, Value} -> 468 | {ok, SessId, Value}; 469 | {N, SessId, _Value} -> 470 | {error, wire_error(N), SessId}; 471 | {error, JErr} -> 472 | {json_error, JErr} 473 | end. 474 | 475 | parse_httpc_result({ok, Result}) -> 476 | %% io:format("Res: ~p\n", [Result]), 477 | case Result of 478 | {{_Vsn, Code, Reason}, Hdrs, Body} -> 479 | if Code == 200 -> 480 | json_decode(Body); 481 | Code == 204 -> %% No Content 482 | ok; 483 | (Code >= 400 andalso Code < 500) orelse Code == 501 -> 484 | {error, {txt, Body}}; 485 | Code == 500 -> json_decode(Body); 486 | true -> 487 | case proplists:get_value("content-type", Hdrs, undefined) 488 | == json:mime_type() of 489 | true -> {cmd_fail, json_decode(Body)}; 490 | false -> {error, {txt, lists:concat( 491 | ["Incorrect response ", Code, " - ", Reason])}} 492 | end 493 | end; 494 | {Code, Body} -> 495 | {error, {txt, lists:concat(["Illformed response ", Code, " - ", Body])}}; 496 | _ -> 497 | {error, {txt, "Illformed response, expected normal response got just request_id"}} 498 | end; 499 | parse_httpc_result({error, Reason}) -> 500 | {error, {txt, Reason}}. 501 | 502 | json_decode(Body) -> 503 | case json:decode(Body) of 504 | {ok, Json, []} -> {ok, Json}; 505 | {ok, _PJson, Rest} -> 506 | {error, {json, "Only partial decode possible, remaining: " ++ Rest}}; 507 | {error, Reason} -> {error, {json, Reason}} 508 | end. 509 | 510 | parse_response_json(JSON) -> 511 | case JSON of 512 | {obj, Dict} -> 513 | SessId = proplists:get_value("sessionId", Dict, null), 514 | Status = proplists:get_value("status", Dict, -1), 515 | Value = proplists:get_value("value", Dict, none), 516 | if Status < 0 -> 517 | {error, "JSON object contained no status field"}; 518 | Value == none -> 519 | {error, "JSON object contained no value"}; 520 | true -> 521 | {Status, SessId, Value} 522 | end; 523 | _ -> 524 | {error, "JSON response is not of object type"} 525 | end. 526 | 527 | %% WIRE Protocol Errors 528 | -spec wire_error(integer()) -> {atom(), string()}. 529 | wire_error(6) -> {'NoSuchDriver' , "A session is either terminated or not started"}; 530 | wire_error(7) -> {'NoSuchElement' , "An element could not be located on the page using the given search parameters."}; 531 | wire_error(8) -> {'NoSuchFrame' , "A request to switch to a frame could not be satisfied because the frame could not be found."}; 532 | wire_error(9) -> {'UnknownCommand' , "The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource."}; 533 | wire_error(10) -> {'StaleElementReference' , "An element command failed because the referenced element is no longer attached to the DOM."}; 534 | wire_error(11) -> {'ElementNotVisible' , "An element command could not be completed because the element is not visible on the page."}; 535 | wire_error(12) -> {'InvalidElementState' , "An element command could not be completed because the element is in an invalid state (e.g. attempting to click a disabled element)."}; 536 | wire_error(13) -> {'UnknownError' , "An unknown server-side error occurred while processing the command."}; 537 | wire_error(15) -> {'ElementIsNotSelectable' , "An attempt was made to select an element that cannot be selected."}; 538 | wire_error(17) -> {'JavaScriptError' , "An error occurred while executing user supplied JavaScript."}; 539 | wire_error(19) -> {'XPathLookupError' , "An error occurred while searching for an element by XPath."}; 540 | wire_error(21) -> {'Timeout' , "An operation did not complete before its timeout expired."}; 541 | wire_error(23) -> {'NoSuchWindow' , "A request to switch to a different window could not be satisfied because the window could not be found."}; 542 | wire_error(24) -> {'InvalidCookieDomain' , "An illegal attempt was made to set a cookie under a different domain than the current page."}; 543 | wire_error(25) -> {'UnableToSetCookie' , "A request to set a cookie's value could not be satisfied."}; 544 | wire_error(26) -> {'UnexpectedAlertOpen' , "A modal dialog was open, blocking this operation."}; 545 | wire_error(27) -> {'NoAlertOpenError' , "An attempt was made to operate on a modal dialog when one was not open."}; 546 | wire_error(28) -> {'ScriptTimeout' , "A script did not complete before its timeout expired."}; 547 | wire_error(29) -> {'InvalidElementCoordinates' , "The coordinates provided to an interactions operation are invalid."}; 548 | wire_error(30) -> {'IMENotAvailable' , "IME was not available."}; 549 | wire_error(31) -> {'IMEEngineActivationFailed' , "An IME engine could not be started."}; 550 | wire_error(32) -> {'InvalidSelector' , "Argument was an invalid selector (e.g. XPath/CSS)."}; 551 | wire_error(33) -> {'SessionNotCreatedException', "A new session could not be created."}; 552 | wire_error(34) -> {'MoveTargetOutOfBounds' , "Target provided for a move action is out of bounds."}. 553 | 554 | 555 | %% BUG in httpc:request, does not follow 303 when POST:ing 556 | %% in R16 it correctly follows 303 redirects, but fails to 557 | %% get the relative location correct... Sigh... 558 | %% TODO: Make the fix more general?? 559 | %% (non-relative location etc...) 560 | httpc_request_bug_fix(Opts, post, Request={_Url, Headers, _, _}) -> 561 | Url = "http://" ++ proplists:get_value("host", Headers) ++ "/", 562 | Timeout = {timeout, timeout(Opts)}, 563 | case httpc:request(post, Request, 564 | [Timeout, {autoredirect, false}], 565 | [{headers_as_is, true}]) of 566 | _Res = {ok, {{_, 303, _}, OutHdrs, _Body}} -> 567 | NewLoc = proplists:get_value("location", OutHdrs, " "), 568 | Res = httpc_request_bug_fix(Opts, get, {Url ++ tl(NewLoc), []}), 569 | Res; 570 | % Fix selenium 571 | _Res = {ok, {{_, 302, _}, OutHdrs, _Body}} -> 572 | Redirect = proplists:get_value("location", OutHdrs, ""), 573 | httpc:request(get, {Redirect, [{"Accept", "application/json"}]}, [Timeout], []); 574 | % Fix ios-driver 575 | _Res = {ok, {{_, 301, _}, OutHdrs, _Body}} -> 576 | Redirect = proplists:get_value("location", OutHdrs, ""), 577 | httpc:request(get, {Redirect, [{"Accept", "application/json"}]}, [Timeout], []); 578 | Res -> 579 | Res 580 | end; 581 | httpc_request_bug_fix(Opts, Method, Request) -> 582 | httpc:request(Method, Request, [{timeout, timeout(Opts)}, {autoredirect, true}], []). 583 | 584 | -------------------------------------------------------------------------------- /src/webdrv_session.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Hans Svensson 3 | %%% @copyright (C) 2013, Quviq AB 4 | %%% 5 | %%% @doc {@link gen_server} wrapper for a WebDriver session. 6 | %%% 7 | %%% A WebDriver session (normally the interaction with one browser instance) is associated 8 | %%% to a SessionId, and the URI of the WebDriver server-side. It quickly 9 | %%% gets painful to carry the id and the URL around, therefore this wrapper module was 10 | %%% created. By using this module, each session is associated to a name (an {@link 11 | %%% atom()}), simplifiying issuing a WebDriver command. 12 | %%% 13 | %%% The implementation is a {@link gen_server}, which keeps the SessionId and the URL (and 14 | %%% a timeout) in its state. A typical interaction with this module could be: 15 | %%%
 16 | %%% webdrv_session:start_session(test, Selenium, webdrv_cap:default_htmlunit(), 10000),
 17 | %%% webdrv_session:set_url(test, SomeUrl),
 18 | %%% PageSource = webdrv_session:get_page_source(test),
 19 | %%% io:format("Source: ~p\n", [PageSource]),
 20 | %%% webdrv_session:stop_session(test). 
21 | %%% 22 | %%% The individual functions are not documented. They have type annotations, for more 23 | %%% information on the particular functions, we refer to (The WebDriver 25 | %%% Wire Protocol). 26 | %%% 27 | %%%

License

28 | %%%
 29 | %%% Permission is hereby granted, free of charge, to any person
 30 | %%% obtaining a copy of this software and associated documentation
 31 | %%% files (the "Software"), to deal in the Software without
 32 | %%% restriction, including without limitation the rights to use, copy,
 33 | %%% modify, merge, publish, distribute, sublicense, and/or sell copies
 34 | %%% of the Software, and to permit persons to whom the Software is
 35 | %%% furnished to do so, subject to the following conditions:
 36 | %%%
 37 | %%% The above copyright notice and this permission notice shall be
 38 | %%% included in all copies or substantial portions of the Software.
 39 | %%%
 40 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 41 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 42 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 43 | %%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 44 | %%% BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 45 | %%% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 46 | %%% CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 47 | %%% SOFTWARE. 
48 | %%% @end 49 | %%%------------------------------------------------------------------- 50 | -module(webdrv_session). 51 | 52 | -include("../include/webdrv.hrl"). 53 | 54 | -behaviour(gen_server). 55 | 56 | -define(TIMEOUT, 30000). 57 | 58 | %% API 59 | -export([execute/3, execute_async/3, get_screenshot/1, get_status/1, 60 | get_window_handle/1, get_window_handles/1, 61 | get_ime_available_engines/1, get_ime_active_engine/1, get_ime_activated/1, 62 | ime_deactivate/1, ime_activate/2, 63 | set_frame/2, set_window_focus/2, close_window/1, set_window_size/3, 64 | set_window_size/4, get_window_size/1, get_window_size/2, set_window_position/3, 65 | set_window_position/4, get_window_position/1, get_window_position/2, 66 | set_window_maximize/1, set_window_maximize/2, get_cookies/1, add_cookie/2, 67 | delete_cookies/1, delete_cookie/2, get_page_source/1, get_page_title/1, 68 | find_element/3, find_elements/3, get_active_element/1, find_element_rel/4, 69 | find_elements_rel/4, click_element/2, submit/2, get_text/2, send_value/3, 70 | send_keys/2, element_name/2, clear_element/2, is_selected_element/2, 71 | is_enabled_element/2, element_attribute/3, are_elements_equal/3, 72 | is_displayed_element/2, get_element_info/2, 73 | element_location/2, is_element_location_in_view/2, element_size/2, 74 | element_css_property/3, get_browser_orientation/1, set_browser_orientation/2, 75 | get_alert_text/1, set_alert_text/2, accept_alert/1, dismiss_alert/1, moveto/4, 76 | click/2, buttondown/2, buttonup/2, doubleclick/1, get_log/2, 77 | get_log_types/1, get_cache_status/1, set_timeout/3, set_async_script_timeout/2, 78 | set_implicit_wait_timeout/2, forward/1, back/1, refresh/1, set_url/2, get_url/1, 79 | start_session/3, start_session/4, stop_session/1, stop/1 80 | ]). 81 | 82 | %% gen_server callbacks 83 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 84 | terminate/2, code_change/3]). 85 | 86 | -record(state, { url 87 | , capabilities 88 | , session_id 89 | , timeout}). 90 | 91 | %%%=================================================================== 92 | %%% API 93 | %%%=================================================================== 94 | 95 | %% @equiv start_session(Name, Url, DesiredCapabilities, null, 5000) 96 | -spec start_session(atom(), url(), capability()) -> 97 | {ok, pid()} | request_error(). 98 | start_session(Name, Url, DesiredCapabilities) -> 99 | start_session(Name, Url, DesiredCapabilities, null, 10000). 100 | 101 | %% @equiv start_session(Name, Url, DesiredCapabilities, null, ConnTimeout) 102 | -spec start_session(atom(), url(), capability(), number()) -> 103 | {ok, pid()} | request_error(). 104 | start_session(Name, Url, DesiredCapabilities, ConnTimeout) -> 105 | start_session(Name, Url, DesiredCapabilities, null, ConnTimeout). 106 | 107 | %% @doc 108 | %% Starts the server (initializing a session) 109 | %% 110 | %% @end 111 | -spec start_session(atom(), url(), capability(), capability(), number()) -> 112 | {ok, pid()} | request_error(). 113 | start_session(Name, Url, DesiredCapabilities, RequiredCapabilities, ConnTimeout) -> 114 | inets:start(), 115 | case webdrv_wire:start_session(#webdrv_opts{ url = Url, timeout = ConnTimeout }, 116 | DesiredCapabilities, RequiredCapabilities) of 117 | {ok, SessionId, Capabilities} -> 118 | gen_server:start_link({local, Name}, ?MODULE, [binary_to_list(SessionId), Url, 119 | Capabilities, ConnTimeout], []); 120 | 121 | Err -> {error, Err} 122 | end. 123 | 124 | -spec stop_session(atom()) -> ok. 125 | stop_session(Name) -> 126 | _Res = gen_server:call(Name, stop_session), 127 | ok. 128 | 129 | -spec stop(atom()) -> ok. 130 | stop(Name) -> 131 | _Res = gen_server:call(Name, stop_session_no_cleanup), 132 | ok. 133 | 134 | -spec get_status(atom()) -> {ok, json()} | request_error(). 135 | get_status(Name) -> 136 | mkRes(gen_server:call(Name, {get_status, sid}, ?TIMEOUT), fun(X) -> X end). 137 | 138 | -spec set_timeout(atom(), string(), number()) -> ok | request_error(). 139 | set_timeout(Name, Type, Timeout) -> 140 | mkRes(gen_server:call(Name, {set_timeout, sid, bs(Type), Timeout}, ?TIMEOUT), ok). 141 | 142 | -spec set_async_script_timeout(atom(), number()) -> ok | request_error(). 143 | set_async_script_timeout(Name, Timeout) -> 144 | mkRes(gen_server:call(Name, {set_async_script_timeout, sid, Timeout}, ?TIMEOUT), ok). 145 | 146 | -spec set_implicit_wait_timeout(atom(), number()) -> ok | request_error(). 147 | set_implicit_wait_timeout(Name, Timeout) -> 148 | mkRes(gen_server:call(Name, {set_implicit_wait_timeout, sid, Timeout}, ?TIMEOUT), ok). 149 | 150 | -spec get_window_handle(atom()) -> {ok, string()} | request_error(). 151 | get_window_handle(Name) -> 152 | mkRes(gen_server:call(Name, {get_window_handle, sid}, ?TIMEOUT), fun check_str/1). 153 | 154 | -spec get_window_handles(atom()) -> {ok, [string()]} | request_error(). 155 | get_window_handles(Name) -> 156 | mkRes(gen_server:call(Name, {get_window_handles, sid}, ?TIMEOUT), 157 | fun(WHs) -> check_list(WHs, fun check_str/1) end). 158 | 159 | -spec set_url(atom(), string()) -> ok | request_error(). 160 | set_url(Name, Url) -> 161 | mkRes(gen_server:call(Name, {set_url, sid, bs(Url)}, ?TIMEOUT), ok). 162 | 163 | -spec get_url(atom()) -> {ok, string()} | request_error(). 164 | get_url(Name) -> 165 | mkRes(gen_server:call(Name, {get_url, sid}, ?TIMEOUT), fun check_str/1). 166 | 167 | -spec forward(atom()) -> ok | request_error(). 168 | forward(Name) -> 169 | mkRes(gen_server:call(Name, {forward, sid}, ?TIMEOUT), ok). 170 | 171 | -spec back(atom()) -> ok | request_error(). 172 | back(Name) -> 173 | mkRes(gen_server:call(Name, {back, sid}, ?TIMEOUT), ok). 174 | 175 | -spec refresh(atom()) -> ok | request_error(). 176 | refresh(Name) -> 177 | mkRes(gen_server:call(Name, {refresh, sid}, ?TIMEOUT), ok). 178 | 179 | %% Better for this function to work with pure json data! 180 | -spec execute(atom(), jsonstr(), jsonlist()) -> {ok, json()} | request_error(). 181 | execute(Name, Script, Args) -> 182 | mkRes(gen_server:call(Name, {execute, sid, Script, Args}, ?TIMEOUT), fun(X) -> X end). 183 | 184 | -spec execute_async(atom(), jsonstr(), jsonlist()) -> {ok, json()} | request_error(). 185 | execute_async(Name, Script, Args) -> 186 | mkRes(gen_server:call(Name, {execute_async, sid, Script, Args}, ?TIMEOUT), fun(X) -> X end). 187 | 188 | -spec get_screenshot(atom()) -> {ok, [char()]} | request_error(). 189 | get_screenshot(Name) -> 190 | mkRes(gen_server:call(Name, {get_screenshot, sid}, ?TIMEOUT), fun check_str/1). 191 | 192 | -spec get_ime_available_engines(atom()) -> {ok, [string()]} | request_error(). 193 | get_ime_available_engines(Name) -> 194 | mkRes(gen_server:call(Name, {get_ime_available_engines, sid}, ?TIMEOUT), 195 | fun(Es) -> check_list(Es, fun check_str/1) end). 196 | 197 | -spec get_ime_active_engine(atom()) -> {ok, string()} | request_error(). 198 | get_ime_active_engine(Name) -> 199 | mkRes(gen_server:call(Name, {get_ime_active_engine, sid}, ?TIMEOUT), fun check_str/1). 200 | 201 | -spec get_ime_activated(atom()) -> {ok, boolean()} | request_error(). 202 | get_ime_activated(Name) -> 203 | mkRes(gen_server:call(Name, {get_ime_activated, sid}, ?TIMEOUT), 204 | fun(B) -> check_bool(B) end). 205 | 206 | -spec ime_deactivate(atom()) -> ok | request_error(). 207 | ime_deactivate(Name) -> 208 | mkRes(gen_server:call(Name, {ime_deactivate, sid}, ?TIMEOUT), ok). 209 | 210 | -spec ime_activate(atom(), string()) -> ok | request_error(). 211 | ime_activate(Name, Engine) -> 212 | mkRes(gen_server:call(Name, {ime_activate, sid, bs(Engine)}, ?TIMEOUT), ok). 213 | 214 | %% Uses raw json!? 215 | -spec set_frame(atom(), json()) -> ok | request_error(). 216 | set_frame(Name, Frame) -> 217 | mkRes(gen_server:call(Name, {set_frame, sid, Frame}, ?TIMEOUT), ok). 218 | 219 | -spec set_window_focus(atom(), string()) -> ok | request_error(). 220 | set_window_focus(Name, Window) -> 221 | mkRes(gen_server:call(Name, {set_window_focus, sid, bs(Window)}, ?TIMEOUT), ok). 222 | 223 | -spec close_window(atom()) -> ok | request_error(). 224 | close_window(Name) -> 225 | mkRes(gen_server:call(Name, {close_window, sid}, ?TIMEOUT), ok). 226 | 227 | -spec set_window_size(atom(), number(), number()) -> ok | request_error(). 228 | set_window_size(Name, Width, Height) -> 229 | mkRes(gen_server:call(Name, {set_window_size, sid, "current", Width, Height}, ?TIMEOUT), ok). 230 | 231 | -spec set_window_size(atom(), string(), number(), number()) -> ok | request_error(). 232 | set_window_size(Name, WindowHandle, Width, Height) -> 233 | mkRes(gen_server:call(Name, {set_window_size, sid, WindowHandle, Width, Height}, ?TIMEOUT), ok). 234 | 235 | -spec get_window_size(atom()) -> {ok, {number(), number()}} | request_error(). 236 | get_window_size(Name) -> 237 | get_window_size(Name, "current"). 238 | 239 | -spec get_window_size(atom(), string()) -> {ok, {number(), number()}} | request_error(). 240 | get_window_size(Name, WindowHandle) -> 241 | mkRes(gen_server:call(Name, {get_window_size, sid, WindowHandle}, ?TIMEOUT), 242 | fun({obj, Dict}) -> 243 | Width = proplists:get_value("width", Dict, 0), 244 | Height = proplists:get_value("height", Dict, 0), 245 | {Width, Height}; 246 | (X) -> {type_error, exp_obj, X} 247 | end). 248 | 249 | -spec set_window_position(atom(), number(), number()) -> ok | request_error(). 250 | set_window_position(Name, X, Y) -> 251 | mkRes(gen_server:call(Name, {set_window_position, sid, X, Y}, ?TIMEOUT), ok). 252 | 253 | -spec set_window_position(atom(), string(), number(), number()) -> ok | request_error(). 254 | set_window_position(Name, WindowHandle, X, Y) -> 255 | mkRes(gen_server:call(Name, {set_window_position, sid, WindowHandle, X, Y}, ?TIMEOUT), ok). 256 | 257 | -spec get_window_position(atom()) -> {ok, {number(), number()}} | request_error(). 258 | get_window_position(Name) -> 259 | get_window_position(Name, "current"). 260 | 261 | -spec get_window_position(atom(), string()) -> {ok, {number(), number()}} | request_error(). 262 | get_window_position(Name, WindowHandle) -> 263 | mkRes(gen_server:call(Name, {get_window_position, sid, WindowHandle}, ?TIMEOUT), 264 | fun({obj, Dict}) -> 265 | X = proplists:get_value("x", Dict, 0), 266 | Y = proplists:get_value("y", Dict, 0), 267 | {X, Y}; 268 | (X) -> {type_error, exp_obj, X} 269 | end). 270 | 271 | -spec set_window_maximize(atom()) -> ok | request_error(). 272 | set_window_maximize(Name) -> 273 | mkRes(gen_server:call(Name, {set_window_maximize, sid}, ?TIMEOUT), ok). 274 | 275 | -spec set_window_maximize(atom(), string()) -> ok | request_error(). 276 | set_window_maximize(Name, WindowHandle) -> 277 | mkRes(gen_server:call(Name, {set_window_maximize, sid, WindowHandle}, ?TIMEOUT), ok). 278 | 279 | -spec get_cookies(atom()) -> {ok, [cookie()]} | request_error(). 280 | get_cookies(Name) -> 281 | mkRes(gen_server:call(Name, {get_cookies, sid}, ?TIMEOUT), fun(X) -> X end). 282 | 283 | -spec add_cookie(atom(), cookie()) -> ok | request_error(). 284 | add_cookie(Name, Cookie) -> 285 | mkRes(gen_server:call(Name, {add_cookie, sid, Cookie}, ?TIMEOUT), ok). 286 | 287 | -spec delete_cookies(atom()) -> ok | request_error(). 288 | delete_cookies(Name) -> 289 | mkRes(gen_server:call(Name, {delete_cookies, sid}, ?TIMEOUT), ok). 290 | 291 | -spec delete_cookie(atom(), string()) -> ok | request_error(). 292 | delete_cookie(Name, CookieName) -> 293 | mkRes(gen_server:call(Name, {delete_cookie, sid, CookieName}, ?TIMEOUT), ok). 294 | 295 | -spec get_page_source(atom()) -> {ok, string()} | request_error(). 296 | get_page_source(Name) -> 297 | mkRes(gen_server:call(Name, {get_page_source, sid}, ?TIMEOUT), fun check_str/1). 298 | 299 | -spec get_page_title(atom()) -> {ok, string()} | request_error(). 300 | get_page_title(Name) -> 301 | mkRes(gen_server:call(Name, {get_page_title, sid}, ?TIMEOUT), fun check_str/1). 302 | 303 | -spec find_element(atom(), string(), string()) -> {ok, string()} | request_error(). 304 | find_element(Name, Strategy, Value) -> 305 | V = unicode:characters_to_binary(Value, unicode, utf8), 306 | mkRes(gen_server:call(Name, {find_element, sid, bs(Strategy), V}, ?TIMEOUT), 307 | fun parse_web_element/1). 308 | 309 | -spec find_elements(atom(), string(), string()) -> {ok, [string()]} | request_error(). 310 | find_elements(Name, Strategy, Value) -> 311 | V = unicode:characters_to_binary(Value, unicode, utf8), 312 | mkRes(gen_server:call(Name, {find_elements, sid, bs(Strategy), V}, ?TIMEOUT), 313 | fun(Es) -> check_list(Es, fun parse_web_element/1) end). 314 | 315 | 316 | -spec get_active_element(atom()) -> {ok, string()} | request_error(). 317 | get_active_element(Name) -> 318 | mkRes(gen_server:call(Name, {get_active_element, sid}, ?TIMEOUT), 319 | fun parse_web_element/1). 320 | 321 | %% Currently undefined return value 322 | -spec get_element_info(atom(), string()) -> {ok, json()} | request_error(). 323 | get_element_info(Name, ElementId) -> 324 | mkRes(gen_server:call(Name, {get_element_info, sid, ElementId}, ?TIMEOUT), fun(X) -> X end). 325 | 326 | -spec find_element_rel(atom(), string(), string(), string()) -> 327 | {ok, string()} | request_error(). 328 | find_element_rel(Name, ElementId, Strategy, Value) -> 329 | mkRes(gen_server:call(Name, {find_element_rel, sid, ElementId, bs(Strategy), bs(Value)}, ?TIMEOUT), 330 | fun parse_web_element/1). 331 | 332 | -spec find_elements_rel(atom(), string(), string(), string()) -> 333 | {ok, [string()]} | request_error(). 334 | find_elements_rel(Name, ElementId, Strategy, Value) -> 335 | mkRes(gen_server:call(Name, {find_elements_rel, sid, ElementId, bs(Strategy), bs(Value)}, ?TIMEOUT), 336 | fun(Es) -> check_list(Es, fun parse_web_element/1) end). 337 | 338 | -spec click_element(atom(), string()) -> ok | request_error(). 339 | click_element(Name, ElementId) -> 340 | mkRes(gen_server:call(Name, {click_element, sid, ElementId}, ?TIMEOUT), ok). 341 | 342 | -spec submit(atom(), string()) -> ok | request_error(). 343 | submit(Name, ElementId) -> 344 | mkRes(gen_server:call(Name, {submit, sid, ElementId}, ?TIMEOUT), ok). 345 | 346 | %% Return type missing in spec!? 347 | -spec get_text(atom(), string()) -> {ok, string()} | request_error(). 348 | get_text(Name, ElementId) -> 349 | mkRes(gen_server:call(Name, {get_text, sid, ElementId}, ?TIMEOUT), fun check_str/1). 350 | 351 | -spec send_value(atom(), string(), string()) -> ok | request_error(). 352 | send_value(Name, ElementId, Value) -> 353 | mkRes(gen_server:call(Name, {send_value, sid, ElementId, bs(Value)}, ?TIMEOUT), ok). 354 | 355 | -spec send_keys(atom(), string()) -> ok | request_error(). 356 | send_keys(Name, Value) -> 357 | mkRes(gen_server:call(Name, {send_keys, sid, bs(Value)}, ?TIMEOUT), ok). 358 | 359 | -spec element_name(atom(), string()) -> {ok, string()} | request_error(). 360 | element_name(Name, ElementId) -> 361 | mkRes(gen_server:call(Name, {element_name, sid, ElementId}, ?TIMEOUT), fun check_str/1). 362 | 363 | -spec clear_element(atom(), string()) -> ok | request_error(). 364 | clear_element(Name, ElementId) -> 365 | mkRes(gen_server:call(Name, {clear_element, sid, ElementId}, ?TIMEOUT), ok). 366 | 367 | -spec is_selected_element(atom(), string()) -> {ok, boolean()} | request_error(). 368 | is_selected_element(Name, ElementId) -> 369 | mkRes(gen_server:call(Name, {is_selected_element, sid, ElementId}, ?TIMEOUT), 370 | fun(X) -> check_bool(X) end). 371 | 372 | -spec is_enabled_element(atom(), string()) -> {ok, boolean()} | request_error(). 373 | is_enabled_element(Name, ElementId) -> 374 | mkRes(gen_server:call(Name, {is_enabled_element, sid, ElementId}, ?TIMEOUT), 375 | fun(X) -> check_bool(X) end). 376 | 377 | -spec element_attribute(atom(), string(), string()) -> 378 | {ok, string() | null} | request_error(). 379 | element_attribute(Name, ElementId, EName) -> 380 | mkRes(gen_server:call(Name, {element_attribute, sid, ElementId, EName}, ?TIMEOUT), 381 | fun(X) -> type_or(check_str(X), check_null(X)) end). 382 | 383 | -spec are_elements_equal(atom(), string(), string()) -> {ok, boolean()} | request_error(). 384 | are_elements_equal(Name, ElementId1, ElementId2) -> 385 | mkRes(gen_server:call(Name, {element_attribute, sid, ElementId1, ElementId2}, ?TIMEOUT), 386 | fun(X) -> check_bool(X) end). 387 | 388 | -spec is_displayed_element(atom(), string()) -> {ok, boolean()} | request_error(). 389 | is_displayed_element(Name, ElementId) -> 390 | mkRes(gen_server:call(Name, {is_displayed_element, sid, ElementId}, ?TIMEOUT), 391 | fun(X) -> check_bool(X) end). 392 | 393 | -spec element_location(atom(), string()) -> {ok, {number(), number()}} | request_error(). 394 | element_location(Name, ElementId) -> 395 | mkRes(gen_server:call(Name, {element_location, sid, ElementId}, ?TIMEOUT), 396 | fun({obj, Dict}) -> 397 | X = proplists:get_value("x", Dict, 0), 398 | Y = proplists:get_value("y", Dict, 0), 399 | {X, Y}; 400 | (X) -> {type_error, exp_obj, X} 401 | end). 402 | 403 | -spec is_element_location_in_view(atom(), string()) -> 404 | {ok, {number(), number()}} | request_error(). 405 | is_element_location_in_view(Name, ElementId) -> 406 | mkRes(gen_server:call(Name, {is_element_location_in_view, sid, ElementId}, ?TIMEOUT), 407 | fun({obj, Dict}) -> 408 | X = proplists:get_value("x", Dict, 0), 409 | Y = proplists:get_value("y", Dict, 0), 410 | {X, Y}; 411 | (X) -> {type_error, exp_obj, X} 412 | end). 413 | 414 | -spec element_size(atom(), string()) -> {ok, {number(), number()}} | request_error(). 415 | element_size(Name, ElementId) -> 416 | mkRes(gen_server:call(Name, {element_size, sid, ElementId}, ?TIMEOUT), 417 | fun({obj, Dict}) -> 418 | Width = proplists:get_value("width", Dict, 0), 419 | Height = proplists:get_value("height", Dict, 0), 420 | {Width, Height}; 421 | (X) -> {type_error, exp_obj, X} 422 | end). 423 | 424 | -spec element_css_property(atom(), string(), string()) -> {ok, string()} | request_error(). 425 | element_css_property(Name, ElementId, Prop) -> 426 | mkRes(gen_server:call(Name, {element_css_property, sid, ElementId, Prop}, ?TIMEOUT), fun check_str/1). 427 | 428 | -spec get_browser_orientation(atom()) -> {ok, orientation()} | request_error(). 429 | get_browser_orientation(Name) -> 430 | mkRes(gen_server:call(Name, {get_browser_orientation, sid}, ?TIMEOUT), fun parse_orientation/1). 431 | 432 | -spec set_browser_orientation(atom(), orientation()) -> ok | request_error(). 433 | set_browser_orientation(Name, Dir) -> 434 | mkRes(gen_server:call(Name, {set_browser_orientation, sid, js_orientation(Dir)}, ?TIMEOUT), ok). 435 | 436 | -spec get_alert_text(atom()) -> {ok, string()} | request_error(). 437 | get_alert_text(Name) -> 438 | mkRes(gen_server:call(Name, {get_alert_text, sid}, ?TIMEOUT), fun check_str/1). 439 | 440 | -spec set_alert_text(atom(), string()) -> ok | request_error(). 441 | set_alert_text(Name, Str) -> 442 | mkRes(gen_server:call(Name, {set_alert_text, sid, bs(Str)}, ?TIMEOUT), ok). 443 | 444 | -spec accept_alert(atom()) -> ok | request_error(). 445 | accept_alert(Name) -> 446 | mkRes(gen_server:call(Name, {accept_alert, sid}, ?TIMEOUT), ok). 447 | 448 | -spec dismiss_alert(atom()) -> ok | request_error(). 449 | dismiss_alert(Name) -> 450 | mkRes(gen_server:call(Name, {dismiss_alert, sid}, ?TIMEOUT), ok). 451 | 452 | -spec moveto(atom(), string(), number(), number()) -> ok | request_error(). 453 | moveto(Name, Elem, XOffSet, YOffSet) -> 454 | mkRes(gen_server:call(Name, {moveto, sid, bs(Elem), XOffSet, YOffSet}, ?TIMEOUT), ok). 455 | 456 | -spec click(atom(), button()) -> ok | request_error(). 457 | click(Name, Button) -> 458 | mkRes(gen_server:call(Name, {click, sid, js_button(Button)}, ?TIMEOUT), ok). 459 | 460 | -spec buttondown(atom(), button()) -> ok | request_error(). 461 | buttondown(Name, Button) -> 462 | mkRes(gen_server:call(Name, {buttondown, sid, js_button(Button)}, ?TIMEOUT), ok). 463 | 464 | -spec buttonup(atom(), button()) -> ok | request_error(). 465 | buttonup(Name, Button) -> 466 | mkRes(gen_server:call(Name, {buttonup, sid, js_button(Button)}, ?TIMEOUT), ok). 467 | 468 | -spec doubleclick(atom()) -> ok | request_error(). 469 | doubleclick(Name) -> 470 | mkRes(gen_server:call(Name, {doubleclick, sid}, ?TIMEOUT), ok). 471 | 472 | %% SKIP Touch, Geo and Local storage for now... 473 | 474 | -spec get_log(atom(), string()) -> {ok, log()} | request_error(). 475 | get_log(Name, LogType) -> 476 | mkRes(gen_server:call(Name, {get_log, sid, bs(LogType)}, ?TIMEOUT), 477 | fun(LogEntries) -> check_list(LogEntries, fun parse_log_entry/1) end). 478 | 479 | -spec get_log_types(atom()) -> {ok, [string()]} | request_error(). 480 | get_log_types(Name) -> 481 | mkRes(gen_server:call(Name, {get_log_types, sid}, ?TIMEOUT), 482 | fun(Types) -> check_list(Types, fun check_str/1) end). 483 | 484 | -spec get_cache_status(atom()) -> {ok, cache_status()} | request_error(). 485 | get_cache_status(Name) -> 486 | mkRes(gen_server:call(Name, {get_cache_status, sid}, ?TIMEOUT), 487 | fun parse_cache_status/1). 488 | 489 | %%%=================================================================== 490 | %%% gen_server callbacks 491 | %%%=================================================================== 492 | 493 | %%-------------------------------------------------------------------- 494 | %% @private 495 | %% @doc 496 | %% Initializes the server 497 | %% 498 | %% @spec init(Args) -> {ok, State} | 499 | %% {ok, State, Timeout} | 500 | %% ignore | 501 | %% {stop, Reason} 502 | %% @end 503 | %%-------------------------------------------------------------------- 504 | init([SessionId, Url, Capabilities, ConnTimeout]) -> 505 | {ok, #state{ url = Url, capabilities = Capabilities, 506 | timeout = ConnTimeout, session_id = SessionId } }. 507 | 508 | %%-------------------------------------------------------------------- 509 | %% @private 510 | %% @doc 511 | %% Handling call messages 512 | %% 513 | %% @spec handle_call(Request, From, State) -> 514 | %% {reply, Reply, State} | 515 | %% {reply, Reply, State, Timeout} | 516 | %% {noreply, State} | 517 | %% {noreply, State, Timeout} | 518 | %% {stop, Reason, Reply, State} | 519 | %% {stop, Reason, State} 520 | %% @end 521 | %%-------------------------------------------------------------------- 522 | handle_call({Fun, sid}, _From, S) -> 523 | Reply = webdrv_wire:Fun(mkOpts(S)), 524 | {reply, Reply, S}; 525 | handle_call({Fun, sid, Arg1}, _From, S) -> 526 | Reply = webdrv_wire:Fun(mkOpts(S), Arg1), 527 | {reply, Reply, S}; 528 | handle_call({Fun, sid, Arg1, Arg2}, _From, S) -> 529 | Reply = webdrv_wire:Fun(mkOpts(S), Arg1, Arg2), 530 | {reply, Reply, S}; 531 | handle_call({Fun, sid, Arg1, Arg2, Arg3}, _From, S) -> 532 | Reply = webdrv_wire:Fun(mkOpts(S), Arg1, Arg2, Arg3), 533 | {reply, Reply, S}; 534 | handle_call(stop_session, _From, S) -> 535 | Reply = webdrv_wire:stop_session(mkOpts(S)), 536 | {stop, normal, Reply, S#state{ session_id = undefined, url = undefined }}; 537 | handle_call(stop_session_no_cleanup, _From, S) -> 538 | {stop, normal, ok, S}; 539 | handle_call(_Request, _From, State) -> 540 | Reply = ok, 541 | {reply, Reply, State}. 542 | 543 | %%-------------------------------------------------------------------- 544 | %% @private 545 | %% @doc 546 | %% Handling cast messages 547 | %% 548 | %% @spec handle_cast(Msg, State) -> {noreply, State} | 549 | %% {noreply, State, Timeout} | 550 | %% {stop, Reason, State} 551 | %% @end 552 | %%-------------------------------------------------------------------- 553 | handle_cast(_Msg, State) -> 554 | {noreply, State}. 555 | 556 | %%-------------------------------------------------------------------- 557 | %% @private 558 | %% @doc 559 | %% Handling all non call/cast messages 560 | %% 561 | %% @spec handle_info(Info, State) -> {noreply, State} | 562 | %% {noreply, State, Timeout} | 563 | %% {stop, Reason, State} 564 | %% @end 565 | %%-------------------------------------------------------------------- 566 | handle_info(_Info, State) -> 567 | {noreply, State}. 568 | 569 | %%-------------------------------------------------------------------- 570 | %% @private 571 | %% @doc 572 | %% This function is called by a gen_server when it is about to 573 | %% terminate. It should be the opposite of Module:init/1 and do any 574 | %% necessary cleaning up. When it returns, the gen_server terminates 575 | %% with Reason. The return value is ignored. 576 | %% 577 | %% @spec terminate(Reason, State) -> void() 578 | %% @end 579 | %%-------------------------------------------------------------------- 580 | terminate(_Reason, _State) -> 581 | ok. 582 | 583 | %%-------------------------------------------------------------------- 584 | %% @private 585 | %% @doc 586 | %% Convert process state when code is changed 587 | %% 588 | %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} 589 | %% @end 590 | %%-------------------------------------------------------------------- 591 | code_change(_OldVsn, State, _Extra) -> 592 | {ok, State}. 593 | 594 | %%%=================================================================== 595 | %%% Internal functions 596 | %%%=================================================================== 597 | mkOpts(#state{ session_id = SId, timeout = T, url = Url }) -> 598 | #webdrv_opts{ url = Url, timeout = T, session_id = SId }. 599 | 600 | parse_web_element({obj, [{"ELEMENT", BElem}]}) -> 601 | binary_to_list(BElem); 602 | parse_web_element(X) -> {type_error, exp_element_obj, X}. 603 | 604 | 605 | -spec parse_log_entry(jsonobj()) -> log_entry(). 606 | parse_log_entry({obj, Dict}) -> 607 | Timestamp = proplists:get_value("timestamp", Dict, -1), 608 | Level = proplists:get_value("level", Dict, ""), 609 | Msg = proplists:get_value("message", Dict, ""), 610 | {Timestamp, Level, Msg}; 611 | parse_log_entry(X) -> {type_error, exp_logentry_obj, X}. 612 | 613 | 614 | -spec parse_cache_status(number()) -> cache_status(). 615 | parse_cache_status(0) -> uncached; 616 | parse_cache_status(1) -> idle; 617 | parse_cache_status(2) -> checking; 618 | parse_cache_status(3) -> downloading; 619 | parse_cache_status(4) -> update_ready; 620 | parse_cache_status(5) -> obsolete; 621 | parse_cache_status(X) -> {type_error, exp_cache_status, X}. 622 | 623 | -spec parse_orientation(binary()) -> orientation(). 624 | parse_orientation(<<"PORTRAIT">>) -> portrait; 625 | parse_orientation(<<"LANDSCAPE">>) -> landscape; 626 | parse_orientation(X) -> {type_error, exp_orientation, X}. 627 | 628 | bs(String) when is_list(String) -> 629 | % unicode:characters_to_binary(String, unicode, utf8); 630 | list_to_binary(String); 631 | bs(BinString) when is_binary(BinString) -> 632 | BinString; 633 | %% bs(X) when is_number(X) -> X; 634 | %% bs(true) -> true; 635 | %% bs(false) -> false; 636 | bs(null) -> null. 637 | 638 | -spec js_button(button()) -> number(). 639 | js_button(left) -> 0; 640 | js_button(middle) -> 1; 641 | js_button(right) -> 2; 642 | js_button(N) -> N. 643 | 644 | -spec js_orientation(orientation()) -> binary(). 645 | js_orientation(portrait) -> <<"PORTRAIT">>; 646 | js_orientation(landscape) -> <<"LANDSCAPE">>. 647 | 648 | check_str(BinString) when is_binary(BinString) -> 649 | unicode:characters_to_list(BinString); 650 | check_str(X) -> 651 | {type_error, exp_binstring, X}. 652 | 653 | check_bool(X) when is_boolean(X) -> X; 654 | check_bool(X) -> {type_error, exp_boolean, X}. 655 | 656 | check_null(null) -> null; 657 | check_null(X) -> {type_error, exp_null, X}. 658 | 659 | check_list(X, F) when is_list(X) -> mkList(X, F, []); 660 | check_list(X, _) -> {type_error, exp_array, X}. 661 | 662 | mkList([], _, Xs) -> lists:reverse(Xs); 663 | mkList([X | Xs], F, Ys) -> 664 | case F(X) of 665 | E = {type_error, _E, _X} -> E; 666 | R -> mkList(Xs, F, [R | Ys]) 667 | end. 668 | 669 | type_or({type_error, E1, X}, {type_error, E2, _X}) -> 670 | {type_error, list_to_atom(lists:concat([E1, "_or_", E2])), X}; 671 | type_or({type_error, _, _}, X) -> X; 672 | type_or(X, _) -> X. 673 | 674 | mkRes({ok, _SId, _Res}, ok) -> ok; 675 | mkRes({ok, _SId, Res}, Fun) -> 676 | case Fun(Res) of 677 | E = {type_error, _E, _X} -> E; 678 | X -> {ok, X} 679 | end; 680 | mkRes(Res, _Fun) -> Res. 681 | -------------------------------------------------------------------------------- /test/webdrv_eqc.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Hans Svensson 3 | %%% @copyright (C) 2013, Quviq AB 4 | %%% @license 5 | %%% Permission is hereby granted, free of charge, to any person 6 | %%% obtaining a copy of this software and associated documentation 7 | %%% files (the "Software"), to deal in the Software without 8 | %%% restriction, including without limitation the rights to use, copy, 9 | %%% modify, merge, publish, distribute, sublicense, and/or sell copies 10 | %%% of the Software, and to permit persons to whom the Software is 11 | %%% furnished to do so, subject to the following conditions: 12 | %%% 13 | %%% The above copyright notice and this permission notice shall be 14 | %%% included in all copies or substantial portions of the Software. 15 | %%% 16 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | %%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 20 | %%% BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 21 | %%% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | %%% CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | %%% SOFTWARE. 24 | %%%------------------------------------------------------------------- 25 | %%% @doc 26 | %%% 27 | %%% @end 28 | %%%------------------------------------------------------------------- 29 | 30 | -module(webdrv_eqc). 31 | 32 | -include_lib("eqc/include/eqc.hrl"). 33 | -include_lib("eqc/include/eqc_statem.hrl"). 34 | 35 | -compile(export_all). 36 | 37 | %% Test definitions -- Adapt to local situation 38 | 39 | -define(SELENIUM_URL, "http://localhost:4444/wd/hub/"). 40 | -define(CHROMEDRIVER_URL, "http://localhost:9515/"). 41 | -define(HTTP_URL, "http://localhost:8088/"). 42 | 43 | -define(ORELSE(X, Y), case X of 44 | true -> true; 45 | __R -> case Y of 46 | true -> true; 47 | _ -> __R 48 | end 49 | end). 50 | 51 | %% {Frequency, {Driver, Browser}} 52 | -define(DRIVERS_AND_BROWSERS, 53 | [{2, {selenium, htmlunit}}, 54 | {1, {selenium, chrome}}, 55 | {1, {selenium, firefox}}, 56 | {1, {chromedriver, chrome}}]). 57 | 58 | -define(WINDOWBASE, "WINDOW-"). 59 | -define(SESSIONBASE, "SESS-"). 60 | 61 | -define(MAX_SESSIONS, 2). 62 | -define(MAX_WINDOWS, 3). 63 | 64 | %% Make sure there is margin to increase size and/or move 65 | %% Chromedriver will refuse to move window off screen! 66 | -define(DEFAULT_HEIGHT, 800). 67 | -define(DEFAULT_WIDTH, 600). 68 | 69 | %% ------ Test state 70 | 71 | %% Note: Windows are Tabs in Chrome, thus setting the size of individual windows isn't 72 | %% really meaningful there. 73 | 74 | %% A browser window/tab 75 | -record(window, { name 76 | , curr_page = unknown 77 | , window_handle 78 | , maximized = false 79 | , size 80 | , position }). 81 | %% A webdriver session 82 | -record(session, { name 83 | , driver 84 | , browser 85 | , curr_window = ?WINDOWBASE ++ "0" 86 | , windows = [] 87 | , ime_engines = [] 88 | , size 89 | , position 90 | , maximized = false }). 91 | 92 | -record(state, { sessions = [] }). 93 | 94 | initial_state() -> 95 | #state{}. 96 | 97 | %% ------ Test generators 98 | fresh_session(Sessions) -> 99 | fresh_session([ Name || #session{ name = Name } <- Sessions], 0). 100 | 101 | fresh_session(Used, N) -> 102 | case lists:member(mk_session_name(N), Used) of 103 | true -> fresh_session(Used, N + 1); 104 | false -> mk_session_name(N) end. 105 | 106 | driver_and_browser() -> 107 | N = lists:sum([ N || {N, _} <-tl(?DRIVERS_AND_BROWSERS)]), 108 | weighted_default(hd(?DRIVERS_AND_BROWSERS), 109 | {N, frequency(tl(?DRIVERS_AND_BROWSERS))}). 110 | 111 | session_name(S) -> 112 | elements([ N || #session{ name = N } <- S#state.sessions ]). 113 | 114 | window() -> 115 | ?LET(N, choose(1,?MAX_WINDOWS), ?WINDOWBASE ++ integer_to_list(N-1)). 116 | 117 | window(Windows) -> 118 | elements([W || #window{ name = W } <- Windows]). 119 | 120 | session_and_window(S) -> 121 | ?LET(Session, session_name(S), 122 | ?LET(Window, weighted_default({3, window((get_session(S, Session))#session.windows)}, 123 | {1, oneof([window(), return("current")])}), 124 | {Session, Window})). 125 | 126 | window_size() -> 127 | {choose(?DEFAULT_WIDTH - 100, ?DEFAULT_WIDTH + 100), 128 | choose(?DEFAULT_HEIGHT - 100, ?DEFAULT_HEIGHT + 100)}. 129 | 130 | rel_element_find() -> 131 | ?LET(Good, weighted_default({5,true}, {1, false}), rel_element_find(Good)). 132 | 133 | rel_element_find(false) -> 134 | element_find(false); 135 | rel_element_find(true) -> 136 | elements([{"tag name", "html", first}, 137 | {"tag name", "hr", last}]). 138 | 139 | element_find() -> 140 | ?LET(Good, weighted_default({5,true}, {1, false}), element_find(Good)). 141 | 142 | element_find(Good) -> 143 | frequency([{1, element_id(Good)}, 144 | {1, element_name(Good)}, 145 | {1, element_css(Good)}, 146 | {1, element_tag(Good)}, 147 | {3, element_xpath(Good)}, 148 | {1, element_link(Good)}, 149 | {1, element_plink(Good)}, 150 | {1, element_class(Good)}]). 151 | 152 | element_id(false) -> 153 | {"id", elements(["this_doesnt_exist", "blafoo"]), false}; 154 | element_id(true) -> 155 | {"id", elements(["id1", "id2", "id3"]), true}. 156 | 157 | element_name(false) -> 158 | {"name", elements(["this_doesnt_exist", "blafoo"]), false}; 159 | element_name(true) -> 160 | {"name", elements(["name1", "name2", "name3"]), true}. 161 | 162 | %% TODO: More advanced css! 163 | element_css(false) -> 164 | {"css selector", elements(["h2", "script"]), false}; 165 | element_css(true) -> 166 | {"css selector", elements(["ul", "li"]), true}. 167 | 168 | element_class(false) -> 169 | {"class name", elements(["class9", "class14"]), false}; 170 | element_class(true) -> 171 | {"class name", elements(["class1", "class2"]), true}. 172 | 173 | element_tag(false) -> 174 | {"tag name", elements(["h2", "script"]), false}; 175 | element_tag(true) -> 176 | {"tag name", elements(["ul", "li"]), true}. 177 | 178 | element_link(false) -> 179 | {"link text", elements(["xxxxxxxx", "adfasdlfs"]), false}; 180 | element_link(true) -> 181 | {"link text", elements(["Link1", "Link2"]), true}. 182 | 183 | element_plink(false) -> 184 | {"partial link text", elements(["xx", "zz"]), false}; 185 | element_plink(true) -> 186 | {"partial link text", elements(["k1", "nk2"]), true}. 187 | 188 | element_xpath(false) -> 189 | frequency([{3, {"xpath", elements(["blafoo", "//foobar", "//@foo"]), false}}, 190 | {3, {"xpath", elements(["html[invalid_expr()]", "html[price==14]"]), invalid}}]); 191 | element_xpath(true) -> 192 | {"xpath", elements(["//body", "/html/body/ul", "/html/body/ul/li", 193 | "//li", "//a[last()]"]), true}. 194 | 195 | %% Some problem in Unity, can't position window at {0, 0} -- Menubar! 196 | window_position() -> 197 | {choose(30, 100), choose(30, 100)}. 198 | 199 | url() -> 200 | elements([ ?HTTP_URL ++ "page0.html" 201 | , ?HTTP_URL ++ "page1.html" 202 | , ?HTTP_URL ++ "page2.html" ]). 203 | 204 | which_timeout() -> 205 | elements(["script", "implicit", "page load"]). 206 | 207 | timeout_val() -> 208 | choose(100, 10000). 209 | 210 | implicit_timeout_val() -> 211 | weighted_default({3, 0}, {1, choose(1, 30)}). 212 | 213 | %% ------ Common pre-/post-conditions 214 | command_precondition_common(S, Cmd) -> 215 | length(S#state.sessions) > 0 216 | orelse lists:member(Cmd, [start_session]). 217 | 218 | precondition_common(S, {call, _, Cmd, _}) -> 219 | length(S#state.sessions) > 0 220 | orelse lists:member(Cmd, [start_session]). 221 | 222 | %% --- start_session 223 | %% Starting a WebDriver session. 224 | %% + Connect to browser 225 | %% + Set a known start page 226 | %% + Name the first window ?WINDOWBASE ++ "0" 227 | %% + Set a reasonable size of the window 228 | start_session(Session, {Driver, Browser}) -> 229 | try 230 | Res = 231 | case webdrv_session:start_session(Session, driver_url(Driver), 232 | webdrv_cap:default_browser(Browser), 8000) of 233 | {ok, _Pid} -> 234 | webdrv_session:set_url(Session, ?HTTP_URL ++ "page1.html"), 235 | webdrv_session:execute(Session, <<"window.name = '" ?WINDOWBASE "0';">>, []), 236 | webdrv_session:set_window_size(Session, ?DEFAULT_WIDTH, ?DEFAULT_HEIGHT), 237 | ok; 238 | {error, {already_started, _Pid}} -> {error, already_started}; 239 | E -> E 240 | end, 241 | Res 242 | catch _:Err -> 243 | {error, Err} 244 | end. 245 | 246 | start_session_args(S) -> 247 | [fresh_session(S#state.sessions), driver_and_browser()]. 248 | 249 | start_session_pre(S) -> 250 | length(S#state.sessions) < ?MAX_SESSIONS. 251 | 252 | start_session_pre(S, [SessName, {_Driver, _Browser}]) -> 253 | length(S#state.sessions) < ?MAX_SESSIONS 254 | andalso not lists:member(SessName, [ N || #session{ name = N } <- S#state.sessions ]). 255 | 256 | start_session_next(S, _Value, [SessName, {Driver, Browser}]) -> 257 | S#state{ sessions = [#session{ name = SessName, 258 | driver = Driver, 259 | browser = Browser, 260 | windows = [#window{ name = ?WINDOWBASE ++ "0", 261 | curr_page = ?HTTP_URL ++ "page1.html" }] } 262 | | S#state.sessions] 263 | }. 264 | 265 | start_session_post(_S, [_SessNameState, _Driver_And_Browser], Res) -> 266 | eq(Res, ok). 267 | 268 | 269 | %% --- stop_session 270 | stop_session(Session) -> 271 | case (catch webdrv_session:stop_session(Session)) of 272 | {'EXIT', _} -> 273 | %% In case the gen_server is hung, be more brutal 274 | catch webdrv_session:stop(Session); 275 | _ -> ok 276 | end. 277 | 278 | stop_session_args(S) -> 279 | [session_name(S)]. 280 | 281 | stop_session_next(S, _V, [Session]) -> 282 | S#state{ sessions = lists:keydelete(Session, #session.name, S#state.sessions) }. 283 | 284 | %% --- set_url 285 | set_url(Session, Url) -> 286 | webdrv_session:set_url(Session, Url). 287 | 288 | set_url_args(S) -> 289 | [session_name(S), url()]. 290 | 291 | set_url_next(S, _V, [Session, Url]) -> 292 | set_curr_page(S, Session, Url). 293 | 294 | 295 | %% --- get_url 296 | get_url(Session) -> 297 | webdrv_session:get_url(Session). 298 | 299 | get_url_args(S) -> 300 | [session_name(S)]. 301 | 302 | get_url_post(S, [Session], Res) -> 303 | case get_curr_page(S, Session) of 304 | unknown -> true; 305 | Page -> eq(Res, {ok, Page}) end. 306 | 307 | 308 | %% --- set_timeout 309 | set_timeout(Session, WhichTimeout, Timeout) -> 310 | webdrv_session:set_timeout(Session, WhichTimeout, Timeout). 311 | 312 | set_timeout_args(S) -> 313 | [session_name(S), which_timeout(), timeout_val()]. 314 | 315 | set_timeout_post(S, [Session, WhichTimeout, _Timeout], Res) -> 316 | ?ORELSE(eq(Res, ok), 317 | (is_chrome(S, Session) 318 | andalso is_selenium(S, Session) 319 | andalso WhichTimeout == "page load")). 320 | 321 | %% --- set_async_script_timeout 322 | set_async_script_timeout(Session, Timeout) -> 323 | webdrv_session:set_async_script_timeout(Session, Timeout). 324 | 325 | set_async_script_timeout_args(S) -> 326 | [session_name(S), timeout_val()]. 327 | 328 | set_async_script_timeout_post(_S, [_Session, _Timeout], Res) -> 329 | eq(Res, ok). 330 | 331 | %% --- set_implicit_wait_timeout 332 | set_implicit_wait_timeout(Session, Timeout) -> 333 | webdrv_session:set_implicit_wait_timeout(Session, Timeout). 334 | 335 | set_implicit_wait_timeout_args(S) -> 336 | [session_name(S), implicit_timeout_val()]. 337 | 338 | set_implicit_wait_timeout_post(_S, [_Session, _Timeout], Res) -> 339 | eq(Res, ok). 340 | 341 | %% --- get_window_handle 342 | get_window_handle(Session) -> 343 | webdrv_session:get_window_handle(Session). 344 | 345 | get_window_handle_args(S) -> 346 | [session_name(S)]. 347 | 348 | get_window_handle_next(S, V, [Session]) -> 349 | set_curr_window_handle(S, Session, V). 350 | 351 | get_window_handle_post(S, [Session], Res) -> 352 | WHandle = get_curr_window_handle(S, Session), 353 | ?ORELSE(eq(Res, WHandle), 354 | WHandle == undefined). %% If we think we know the handle it should match! 355 | 356 | %% --- get_window_handles 357 | get_window_handles(Session) -> 358 | webdrv_session:get_window_handles(Session). 359 | 360 | get_window_handles_args(S) -> 361 | [session_name(S)]. 362 | 363 | %% Let's be happy if the length is ok. TODO: check the names 364 | get_window_handles_post(S, [Session], Res) -> 365 | FixRes = case Res of 366 | {ok, Hs} when is_list(Hs) -> {ok, length(Hs)}; 367 | _ -> Res end, 368 | eq(FixRes, {ok, n_windows(S, Session)}). 369 | 370 | %% --- forward 371 | forward(Session) -> 372 | webdrv_session:forward(Session). 373 | 374 | forward_args(S) -> 375 | [session_name(S)]. 376 | 377 | forward_next(S, _V, [Session]) -> 378 | clear_curr_page(S, Session). 379 | 380 | forward_post(_S, [_Session], Res) -> 381 | eq(Res, ok). 382 | 383 | %% --- back 384 | back(Session) -> 385 | webdrv_session:back(Session). 386 | 387 | back_args(S) -> 388 | [session_name(S)]. 389 | 390 | back_next(S, _V, [Session]) -> 391 | clear_curr_page(S, Session). 392 | 393 | back_post(_S, [_Session], Res) -> 394 | eq(Res, ok). 395 | 396 | %% --- refresh 397 | refresh(Session) -> 398 | webdrv_session:refresh(Session). 399 | 400 | refresh_args(S) -> 401 | [session_name(S)]. 402 | 403 | refresh_post(_S, [_Session], Res) -> 404 | eq(Res, ok). 405 | 406 | %% --- get_cookies 407 | get_cookies(Session) -> 408 | webdrv_session:get_cookies(Session). 409 | 410 | get_cookies_args(S) -> 411 | [session_name(S)]. 412 | 413 | get_cookies_next(S, _V, [Session]) -> 414 | clear_curr_page(S, Session). 415 | 416 | get_cookies_post(_S, [_Session], Res) -> 417 | eq(Res, {ok, []}). %% Until we model add_cookie ;-) 418 | 419 | %% TODO: add_cookie, delete_cookie(s) 420 | 421 | %% --- get_page_source 422 | get_page_source(Session) -> 423 | webdrv_session:get_page_source(Session). 424 | 425 | get_page_source_args(S) -> 426 | [session_name(S)]. 427 | 428 | get_page_source_next(S, _V, [Session]) -> 429 | clear_curr_page(S, Session). 430 | 431 | %% TODO: really check we know what to expect... 432 | get_page_source_post(_S, [_Session], Res) -> 433 | res_ok(Res). 434 | 435 | %% --- get_page_title 436 | get_page_title(Session) -> 437 | webdrv_session:get_page_title(Session). 438 | 439 | get_page_title_args(S) -> 440 | [session_name(S)]. 441 | 442 | get_page_title_next(S, _V, [Session]) -> 443 | clear_curr_page(S, Session). 444 | 445 | %% TODO: really check we know what to expect... 446 | get_page_title_post(_S, [_Session], Res) -> 447 | res_ok(Res). 448 | 449 | %% --- set_window_focus 450 | set_window_focus({Session, Window}) -> 451 | webdrv_session:set_window_focus(Session, Window). 452 | 453 | set_window_focus_args(S) -> 454 | [session_and_window(S)]. 455 | 456 | set_window_focus_pre(S, [{Session, _Window}]) -> 457 | is_session(S, Session). 458 | 459 | set_window_focus_next(S, _V, [{Session, Window}]) -> 460 | case get_window(S, Session, Window) of 461 | not_found -> S; 462 | W = #window{} -> set_curr_window(S, Session, W#window.name) end. 463 | 464 | set_window_focus_post(_S, [{_Session, "current"}], Res) -> 465 | res_error(Res, 'NoSuchWindow'); 466 | set_window_focus_post(S, [{Session, Window}], Res) -> 467 | case get_window(S, Session, Window) of 468 | not_found -> res_error(Res, 'NoSuchWindow'); 469 | #window{} -> eq(Res, ok) end. 470 | 471 | %% --- get_window_size 472 | get_window_size({Session, Window}) -> 473 | timer:sleep(50), %% Allow time for size change to be made. 474 | if Window == "current" -> 475 | webdrv_session:get_window_size(Session); 476 | true -> 477 | webdrv_session:get_window_size(Session, Window) 478 | end. 479 | 480 | get_window_size_args(S) -> 481 | [session_and_window(S)]. 482 | 483 | get_window_size_pre(S, [{Session, _Window}]) -> 484 | case get_session(S, Session) of 485 | false -> false; 486 | #session{browser = B, driver = D, windows = Ws } -> 487 | %% There is a bug in the version of chromedriver used by selenium! 488 | (length(Ws) == 1 orelse B /= chrome orelse D /= selenium) 489 | end. 490 | 491 | get_window_size_post(S, [{Session, Window}], Res) -> 492 | case get_window(S, Session, Window) of 493 | not_found -> %% This *should* be an error, but both Selenium and Chromedriver 494 | %% returns the size of the *current* window!? 495 | %% res_error(Res, 'NoSuchWindow'); 496 | res_ok(Res); 497 | #window{ } -> 498 | Size = get_size(S, Session), 499 | case Size of 500 | undefined -> res_ok(Res); 501 | Size -> eq(Res, {ok, Size}) end 502 | end. 503 | 504 | get_size(S, Session) -> 505 | case has_tabs(S, Session) of 506 | true -> 507 | Sess = get_session(S, Session), 508 | Sess#session.size; 509 | false -> 510 | Win = get_curr_window(S, Session), 511 | Win#window.size 512 | end. 513 | 514 | %% --- set_window_size 515 | set_window_size({Session, Window}, {Width, Height}) -> 516 | if Window == "current" -> 517 | webdrv_session:set_window_size(Session, Width, Height); 518 | true -> 519 | webdrv_session:set_window_size(Session, Window, Width, Height) 520 | end. 521 | 522 | set_window_size_args(S) -> 523 | [session_and_window(S), window_size()]. 524 | 525 | set_window_size_pre(S, [{Session, _Window}, _Size]) -> 526 | is_session(S, Session). 527 | 528 | set_window_size_next(S, _V, [{Session, _Window}, Size]) -> 529 | case has_tabs(S, Session) of 530 | true -> 531 | Sess = get_session(S, Session), 532 | set_session(S, Sess#session{ size = Size, maximized = false }); 533 | false -> 534 | Win = get_curr_window(S, Session), 535 | set_window(S, Session, Win#window{ size = Size, maximized = false }) 536 | end. 537 | 538 | set_window_size_post(_S, [{_Session, _Window}, _Size], Res) -> 539 | eq(Res, ok). 540 | 541 | %% --- get_window_position 542 | get_window_position({Session, Window}) -> 543 | timer:sleep(50), %% Allow time for position change to be made. 544 | if Window == "current" -> 545 | webdrv_session:get_window_position(Session); 546 | true -> 547 | webdrv_session:get_window_position(Session, Window) 548 | end. 549 | 550 | get_window_position_args(S) -> 551 | [session_and_window(S)]. 552 | 553 | get_window_position_pre(S, [{Session, _Window}]) -> 554 | case get_session(S, Session) of 555 | false -> false; 556 | #session{browser = B, driver = D, windows = Ws } -> 557 | %% There is a bug in the version of chromedriver used by selenium! 558 | (length(Ws) == 1 orelse B /= chrome orelse D /= selenium) 559 | end. 560 | 561 | get_window_position_post(S, [{Session, Window}], Res) -> 562 | case get_window(S, Session, Window) of 563 | not_found -> %% This *should* be an error, but both Selenium and Chromedriver 564 | %% returns the position of the *current* window!? 565 | %% res_error(Res, 'NoSuchWindow'); 566 | res_ok(Res); 567 | #window{ } -> Pos = get_position(S, Session), 568 | case Pos of 569 | undefined -> res_ok(Res); 570 | Position -> eq(Res, {ok, Position}) end 571 | end. 572 | 573 | get_position(S, Session) -> 574 | case has_tabs(S, Session) of 575 | true -> 576 | Sess = get_session(S, Session), 577 | Sess#session.position; 578 | false -> 579 | Win = get_curr_window(S, Session), 580 | Win#window.position 581 | end. 582 | 583 | %% --- set_window_position 584 | set_window_position({Session, Window}, {Width, Height}) -> 585 | if Window == "current" -> 586 | webdrv_session:set_window_position(Session, Width, Height); 587 | true -> 588 | webdrv_session:set_window_position(Session, Window, Width, Height) 589 | end. 590 | 591 | set_window_position_args(S) -> 592 | [session_and_window(S), window_position()]. 593 | 594 | set_window_position_pre(S, [{Session, _Window}, _Position]) -> 595 | is_session(S, Session). 596 | 597 | set_window_position_next(S, _V, [{Session, _Window}, Position]) -> 598 | case has_tabs(S, Session) of 599 | true -> 600 | Sess = get_session(S, Session), 601 | set_session(S, Sess#session{ position = Position, maximized = false }); 602 | false -> 603 | Win = get_curr_window(S, Session), 604 | set_window(S, Session, Win#window{ position = Position, maximized = false }) 605 | end. 606 | 607 | set_window_position_post(_S, [{_Session, _Window}, _Position], Res) -> 608 | eq(Res, ok). 609 | 610 | %% --- open_window 611 | open_window(Session, Window) -> 612 | webdrv_session:set_url(Session, ?HTTP_URL ++ "windows.html"), 613 | {ok, E} = webdrv_session:find_element(Session, "name", Window), 614 | webdrv_session:click_element(Session, E), 615 | webdrv_session:back(Session), 616 | webdrv_session:set_window_focus(Session, Window). 617 | 618 | open_window_args(S) -> 619 | [session_name(S), window()]. 620 | 621 | open_window_next(S, _V, [Session, Window]) -> 622 | Sess = get_session(S, Session), 623 | S1 = set_session(S, Sess#session{ curr_window = Window }), 624 | set_window(S1, Session, #window{ name = Window }). 625 | 626 | %% --- close_window 627 | %% Make sure not to close the 'last' window; that is stop_session! 628 | close_window(Session, Window) -> 629 | Res = webdrv_session:close_window(Session), 630 | ok = webdrv_session:set_window_focus(Session, Window), 631 | Res. 632 | 633 | close_window_args(S) -> 634 | ?LET({Session, Ws}, 635 | elements([ {N, Ws} || #session{ name = N, windows = Ws } <- S#state.sessions, 636 | length(Ws) > 1 ]), 637 | [Session, elements([ W || #window{ name = W } <- Ws] -- 638 | [get_curr_window(S, Session)])]). 639 | 640 | close_window_pre(S) -> 641 | [] /= [ ok || #session{ windows = Ws } <- S#state.sessions, 642 | length(Ws) > 1 ]. 643 | 644 | close_window_pre(S, [Session, Window]) -> 645 | case get_session(S, Session) of 646 | false -> false; 647 | #session{windows = Ws, curr_window = W } -> 648 | length(Ws) > 1 andalso W /= Window 649 | end. 650 | 651 | close_window_next(S, _V, [Session, Window]) -> 652 | Sess = get_session(S, Session), 653 | set_session(S, Sess#session{ windows = lists:keydelete(Sess#session.curr_window, 654 | #window.name, 655 | Sess#session.windows) 656 | , curr_window = Window }). 657 | 658 | close_window_post(_S, [_Session, _Window], Res) -> 659 | eq(Res, ok). 660 | 661 | %% --- maximize_window 662 | set_window_maximize({Session, Window}) -> 663 | if Window == "current" -> 664 | webdrv_session:set_window_maximize(Session); 665 | true -> 666 | webdrv_session:set_window_maximize(Session, Window) 667 | end. 668 | 669 | set_window_maximize_args(S) -> 670 | [ session_and_window(S) ]. 671 | 672 | set_window_maximize_pre(S, [{Session, _Window}]) -> 673 | case get_session(S, Session) of 674 | false -> false; 675 | #session{ browser = B, driver = D, maximized = M } -> 676 | %% Bug in chromedriver used by selenium. Hangs if maximized twice! 677 | not M orelse B /= chrome orelse D /= selenium 678 | end. 679 | 680 | set_window_maximize_next(S, _V, [{Session, _Window}]) -> 681 | case has_tabs(S, Session) of 682 | true -> 683 | Sess = get_session(S, Session), 684 | set_session(S, Sess#session{ size = undefined, 685 | position = undefined, 686 | maximized = true }); 687 | false -> 688 | Win = get_curr_window(S, Session), 689 | set_window(S, Session, Win#window{ size = undefined, 690 | position = undefined, 691 | maximized = true}) 692 | end. 693 | 694 | set_window_maximize_post(_S, [{_Session, _Window}], Res) -> 695 | eq(Res, ok). 696 | 697 | %% --- get_screenshot 698 | get_screenshot(Session) -> 699 | webdrv_session:get_screenshot(Session). 700 | 701 | get_screenshot_args(S) -> 702 | [session_name(S)]. 703 | 704 | get_screenshot_pre(S, [Session]) -> 705 | case get_session(S, Session) of 706 | false -> false; 707 | #session{browser = B, driver = D, windows = Ws } -> 708 | B /= htmlunit 709 | %% Bug in Chromedriver2, only the last opened window can be screenshot. 710 | %% This is a bit more restrictive, but let's be happy with that... 711 | andalso (B /= chrome orelse D /= chromedriver orelse length(Ws) == 1) 712 | end. 713 | 714 | get_screenshot_post(_S, [_Session], Res) -> 715 | res_ok(Res). 716 | 717 | %% --- get_cache_status 718 | get_cache_status(Session) -> 719 | webdrv_session:get_cache_status(Session). 720 | 721 | get_cache_status_args(S) -> 722 | [session_name(S)]. 723 | 724 | get_cache_status_pre(S, [Session]) -> 725 | case get_session(S, Session) of 726 | false -> false; 727 | #session{} -> not is_selenium(S, Session) 728 | end. 729 | 730 | get_cache_status_post(_S, [_Session], Res) -> 731 | res_ok(Res). 732 | 733 | %% --- find_element 734 | find_element(Session, Strategy, Value) -> 735 | webdrv_session:set_url(Session, ?HTTP_URL ++ "elements.html"), 736 | Res = webdrv_session:find_element(Session, Strategy, Value), 737 | webdrv_session:back(Session), 738 | Res. 739 | 740 | find_element_command(S) -> 741 | ?LET({Strategy, Value, IsOk}, element_find(), 742 | {call, ?MODULE, find_element, [session_name(S), Strategy, Value], IsOk}). 743 | 744 | find_element_post(S, [Session, _Strategy, _Value], Res, IsOk) -> 745 | IsHtmlUnit = is_headless(S, Session), 746 | case IsOk of 747 | true -> res_ok(Res); 748 | false -> res_error(Res, 'NoSuchElement'); 749 | invalid when IsHtmlUnit -> res_error(Res, 'UnknownError'); 750 | invalid -> res_error(Res, 'InvalidSelector') 751 | end. 752 | 753 | %% --- find_elements 754 | find_elements(Session, Strategy, Value) -> 755 | webdrv_session:set_url(Session, ?HTTP_URL ++ "elements.html"), 756 | Res = webdrv_session:find_elements(Session, Strategy, Value), 757 | webdrv_session:back(Session), 758 | Res. 759 | 760 | find_elements_command(S) -> 761 | ?LET({Strategy, Value, IsOk}, element_find(), 762 | {call, ?MODULE, find_elements, [session_name(S), Strategy, Value], IsOk}). 763 | 764 | find_elements_post(S, [Session, _Strategy, _Value], Res, IsOk) -> 765 | IsHtmlUnit = is_headless(S, Session), 766 | case IsOk of 767 | true -> case Res of 768 | {ok, Es} when is_list(Es) -> true; 769 | _ -> eq(Res, {ok, ''}) end; 770 | false -> eq(Res, {ok, []}); 771 | invalid when IsHtmlUnit -> res_error(Res, 'UnknownError'); 772 | invalid -> res_error(Res, 'InvalidSelector') 773 | end. 774 | 775 | %% --- find_element_rel 776 | find_element_rel(Session, {Strategy1, Value1}, {Strategy2, Value2}) -> 777 | webdrv_session:set_url(Session, ?HTTP_URL ++ "elements.html"), 778 | RelE = case webdrv_session:find_element(Session, Strategy1, Value1) of 779 | {ok, E} -> E; 780 | _ -> "not_likely_to_be_an_id" 781 | end, 782 | Res = webdrv_session:find_element_rel(Session, RelE, Strategy2, Value2), 783 | webdrv_session:back(Session), 784 | Res. 785 | 786 | find_element_rel_command(S) -> 787 | ?LET({{Strategy1, Value1, IsOk1}, {Strategy2, Value2, IsOk2}}, 788 | {rel_element_find(), element_find()}, 789 | {call, ?MODULE, find_element_rel, 790 | [session_name(S), {Strategy1, Value1}, {Strategy2, Value2}], {IsOk1, IsOk2}}). 791 | 792 | find_element_rel_post(S, [Session, _SV1, {Strategy2, Value2}], Res, {IsOk1, IsOk2}) -> 793 | IsHtmlUnit = is_headless(S, Session), 794 | IsSelenium = is_selenium(S, Session), 795 | XPathAbs = case {Strategy2, Value2} of 796 | {"xpath", [$/ | _]} -> true; 797 | _ -> false end, 798 | case {IsOk1, IsOk2} of 799 | {false, _} when IsSelenium -> res_error(Res, 'UnknownError'); 800 | {false, _} -> res_error(Res, 'StaleElementReference'); 801 | {invalid, _} when IsSelenium -> res_error(Res, 'UnknownError'); 802 | {invalid, _} -> res_error(Res, 'StaleElementReference'); 803 | {first, true} -> res_ok(Res); 804 | {last, true} when XPathAbs -> res_ok(Res); 805 | {last, true} -> res_error(Res, 'NoSuchElement'); 806 | {_, false} -> res_error(Res, 'NoSuchElement'); 807 | {_, invalid} when IsHtmlUnit -> res_error(Res, 'UnknownError'); 808 | {_, invalid} -> res_error(Res, 'InvalidSelector') 809 | end. 810 | 811 | %% --- find_elements_rel 812 | find_elements_rel(Session, {Strategy1, Value1}, {Strategy2, Value2}) -> 813 | webdrv_session:set_url(Session, ?HTTP_URL ++ "elements.html"), 814 | RelE = case webdrv_session:find_element(Session, Strategy1, Value1) of 815 | {ok, E} -> E; 816 | _ -> "not_likely_to_be_an_id" 817 | end, 818 | Res = webdrv_session:find_elements_rel(Session, RelE, Strategy2, Value2), 819 | webdrv_session:back(Session), 820 | Res. 821 | 822 | find_elements_rel_command(S) -> 823 | ?LET({{Strategy1, Value1, IsOk1}, {Strategy2, Value2, IsOk2}}, 824 | {rel_element_find(), element_find()}, 825 | {call, ?MODULE, find_elements_rel, 826 | [session_name(S), {Strategy1, Value1}, {Strategy2, Value2}], {IsOk1, IsOk2}}). 827 | 828 | find_elements_rel_post(S, [Session, _SV1, {Strategy2, Value2}], Res, {IsOk1, IsOk2}) -> 829 | IsHtmlUnit = is_headless(S, Session), 830 | IsSelenium = is_selenium(S, Session), 831 | XPathAbs = case {Strategy2, Value2} of 832 | {"xpath", [$/ | _]} -> true; 833 | _ -> false end, 834 | case {IsOk1, IsOk2} of 835 | {false, _} when IsSelenium -> res_error(Res, 'UnknownError'); 836 | {false, _} -> res_error(Res, 'StaleElementReference'); 837 | {invalid, _} when IsSelenium -> res_error(Res, 'UnknownError'); 838 | {invalid, _} -> res_error(Res, 'StaleElementReference'); 839 | {first, true} -> res_ok(Res); 840 | {last, true} when XPathAbs -> res_ok(Res); 841 | {last, true} -> eq(Res, {ok, []}); 842 | {_, false} -> eq(Res, {ok, []}); 843 | {_, invalid} when IsHtmlUnit -> res_error(Res, 'UnknownError'); 844 | {_, invalid} -> res_error(Res, 'InvalidSelector') 845 | end. 846 | 847 | %% --- get_ime_available_engines 848 | get_ime_available_engines(Session) -> 849 | webdrv_session:get_ime_available_engines(Session). 850 | 851 | get_ime_available_engines_args(S) -> 852 | [session_name(S)]. 853 | 854 | get_ime_available_engines_post(S, [Session], Res) -> 855 | ime_error(S, Session, Res). 856 | 857 | ime_error(S, Session, Res) -> 858 | case is_selenium(S, Session) of 859 | true -> res_error(Res, 'UnknownError'); 860 | false -> case Res of 861 | {html_error, _} -> true; 862 | _ -> eq(Res, {html_error, blablabla}) 863 | end 864 | end. 865 | 866 | %% --- get_ime_active_engine 867 | get_ime_active_engine(Session) -> 868 | webdrv_session:get_ime_active_engine(Session). 869 | 870 | get_ime_active_engine_args(S) -> 871 | [session_name(S)]. 872 | 873 | get_ime_active_engine_post(S, [Session], Res) -> 874 | ime_error(S, Session, Res). 875 | 876 | %% --- get_ime_activated 877 | get_ime_activated(Session) -> 878 | webdrv_session:get_ime_activated(Session). 879 | 880 | get_ime_activated_args(S) -> 881 | [session_name(S)]. 882 | 883 | get_ime_activated_post(S, [Session], Res) -> 884 | ime_error(S, Session, Res). 885 | 886 | %% --- get_ime_activate 887 | %% get_ime_activate(Session, Engine) -> 888 | %% webdrv_session:get_ime_activate(Session). 889 | 890 | %% get_ime_activate_args(S) -> 891 | %% [session_name(S), ime_engine()]. 892 | 893 | %% get_ime_activate_post(S, [Session, _Engine], Res) -> 894 | %% ime_error(S, Session, Res). 895 | 896 | 897 | %% ------ Property 898 | invariant(_S) -> 899 | true. 900 | 901 | weight(_S, open_window) -> 6; 902 | weight(_S, stop_session) -> 1; 903 | weight(_S, _Cmd) -> 3. 904 | 905 | prop_webdriver() -> 906 | ?SETUP(fun() -> catch webdrv_http_srv:start(), fun() -> ok end end, 907 | ?FORALL(Cmds, more_commands(4, commands(?MODULE)), 908 | begin 909 | {H, S, Res} = run_commands(?MODULE,Cmds), 910 | [ stop_session(Sess#session.name) || Sess <- S#state.sessions ], 911 | aggregate(command_names(Cmds), 912 | pretty_commands(?MODULE, Cmds, {H, S, Res}, 913 | Res == ok)) 914 | end)). 915 | 916 | 917 | the_prop() -> 918 | prop_webdriver(). 919 | 920 | test() -> 921 | test({20, s}). 922 | 923 | test(N) when is_integer(N) -> 924 | eqc:quickcheck(eqc:numtests(N, the_prop())); 925 | test({N, s}) -> 926 | eqc:quickcheck(eqc:testing_time(N, the_prop())); 927 | test({N, min}) -> 928 | test({60*N, s}); 929 | test({N, m}) -> 930 | test({60*N, s}). 931 | 932 | check() -> 933 | check(false). 934 | 935 | check(WState) -> 936 | if WState -> 937 | eqc:check(eqc_statem:show_states(the_prop())); 938 | true -> 939 | eqc:check(the_prop()) 940 | end. 941 | 942 | recheck() -> 943 | eqc:recheck(the_prop()). 944 | 945 | 946 | %% ------ State manipulation 947 | get_session(State, Name) -> 948 | lists:keyfind(Name, #session.name, State#state.sessions). 949 | 950 | set_session(State, Session) -> 951 | State#state{ sessions = lists:keyreplace(Session#session.name, #session.name, 952 | State#state.sessions, Session) }. 953 | 954 | get_window(State, SName, "current") -> 955 | get_curr_window(State, SName); 956 | get_window(State, SName, WName) -> 957 | Session = lists:keyfind(SName, #session.name, State#state.sessions), 958 | case lists:keyfind(WName, #window.name, Session#session.windows) of 959 | false -> not_found; 960 | W -> W 961 | end. 962 | 963 | set_window(State, SName, Window) -> 964 | Session = get_session(State, SName), 965 | set_session(State, 966 | Session#session{ windows = 967 | lists:keystore(Window#window.name, #window.name, 968 | Session#session.windows, Window) }). 969 | 970 | get_curr_window(State, Name) -> 971 | Session = lists:keyfind(Name, #session.name, State#state.sessions), 972 | lists:keyfind(Session#session.curr_window, #window.name, Session#session.windows). 973 | 974 | set_curr_window(State, Name, Window) -> 975 | Session = get_session(State, Name), 976 | set_session(State, Session#session{ curr_window = Window }). 977 | 978 | get_curr_window_handle(State, Name) -> 979 | (get_curr_window(State, Name))#window.window_handle. 980 | 981 | set_curr_window_handle(State, Name, Handle) -> 982 | Win = get_curr_window(State, Name), 983 | set_window(State, Name, Win#window{ window_handle = Handle}). 984 | 985 | get_curr_page(State, Name) -> 986 | Window = get_curr_window(State, Name), 987 | Window#window.curr_page. 988 | 989 | set_curr_page(State, Name, Url) -> 990 | Window = get_curr_window(State, Name), 991 | set_window(State, Name, Window#window{ curr_page = Url }). 992 | 993 | clear_curr_page(State, Name) -> set_curr_page(State, Name, unknown). 994 | 995 | %% ------ Helper functions 996 | mk_session_name(N) -> 997 | list_to_atom(lists:concat([?SESSIONBASE, N])). 998 | 999 | driver_url(selenium) -> ?SELENIUM_URL; 1000 | driver_url(chromedriver) -> ?CHROMEDRIVER_URL. 1001 | 1002 | is_session(S, Session) -> 1003 | get_session(S, Session) /= false. 1004 | 1005 | is_headless(S, Session) -> 1006 | browser(S, Session) == htmlunit. 1007 | 1008 | has_tabs(S, Session) -> 1009 | is_chrome(S, Session). 1010 | 1011 | is_chrome(S, Session) -> 1012 | browser(S, Session) == chrome. 1013 | 1014 | is_firefox(S, Session) -> 1015 | browser(S, Session) == firefox. 1016 | 1017 | is_selenium(S, Session) -> 1018 | driver(S, Session) == selenium. 1019 | 1020 | browser(S, Session) -> 1021 | (get_session(S, Session))#session.browser. 1022 | 1023 | driver(S, Session) -> 1024 | (get_session(S, Session))#session.driver. 1025 | 1026 | n_windows(S, Session) -> 1027 | length((get_session(S, Session))#session.windows). 1028 | 1029 | res_ok(Res) -> 1030 | case Res of 1031 | {ok, _} -> true; 1032 | ok -> true; 1033 | _ -> Res 1034 | end. 1035 | 1036 | res_error(Res, Err) -> 1037 | case Res of 1038 | {error, Err2} -> 1039 | match_err(Err, Err2); 1040 | {error, Err2, _} -> 1041 | match_err(Err, Err2); 1042 | _ -> eq(Res, {error, Err}) 1043 | end. 1044 | 1045 | %% To get some meaningful postcondition... 1046 | match_err(Err1, Err2) -> 1047 | case match_err_(Err1, Err2) of 1048 | true -> true; 1049 | false -> eq({error, Err2}, {error, Err1}) 1050 | end. 1051 | 1052 | match_err_(Err, Err) -> true; 1053 | match_err_(Err, L) when is_list(L) -> 1054 | lists:member(Err, L); 1055 | match_err_(Err, T) when is_tuple(T) -> 1056 | match_err_(Err, tuple_to_list(T)); 1057 | match_err_(_, _) -> false. 1058 | --------------------------------------------------------------------------------